我有这个清单(List<String>
):
["a", "b", null, "c", null, "d", "e"]
我想要这样的东西:
[["a", "b"], ["c"], ["d", "e"]]
换句话说,我想使用null
值作为分隔符将列表拆分为子列表,以获得列表列表(List<List<String>>
)。我正在寻找Java 8解决方案。我已经尝试过,Collectors.partitioningBy
但是我不确定这是我要找的东西。谢谢!
我有这个清单(List<String>
):
["a", "b", null, "c", null, "d", "e"]
我想要这样的东西:
[["a", "b"], ["c"], ["d", "e"]]
换句话说,我想使用null
值作为分隔符将列表拆分为子列表,以获得列表列表(List<List<String>>
)。我正在寻找Java 8解决方案。我已经尝试过,Collectors.partitioningBy
但是我不确定这是我要找的东西。谢谢!
Answers:
我目前提出的唯一解决方案是实现自己的自定义收集器。
在阅读解决方案之前,我想添加一些有关此的注释。我将这个问题更多地看作是编程练习,我不确定是否可以使用并行流来完成。
因此,您必须意识到,如果并行运行管道,它将无声地中断。
这不是理想的行为,应该避免。这就是为什么我在组合器部分(而不是(l1, l2) -> {l1.addAll(l2); return l1;}
)中引发异常,因为在组合两个列表时会并行使用它,以便您有一个异常而不是错误的结果。
同样,由于列表复制,这种方法也不是很有效(尽管它使用本机方法来复制基础数组)。
因此,这是收集器的实现:
private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
final List<String> current = new ArrayList<>();
return Collector.of(() -> new ArrayList<List<String>>(),
(l, elem) -> {
if (sep.test(elem)) {
l.add(new ArrayList<>(current));
current.clear();
}
else {
current.add(elem);
}
},
(l1, l2) -> {
throw new RuntimeException("Should not run this in parallel");
},
l -> {
if (current.size() != 0) {
l.add(current);
return l;
}
);
}
以及如何使用它:
List<List<String>> ll = list.stream().collect(splitBySeparator(Objects::isNull));
输出:
[[a, b], [c], [d, e]]
private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
return Collector.of(() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>())),
(l, elem) -> {if(sep.test(elem)){l.add(new ArrayList<>());} else l.get(l.size()-1).add(elem);},
(l1, l2) -> {l1.get(l1.size() - 1).addAll(l2.remove(0)); l1.addAll(l2); return l1;});
}
这使有关并行性的段落过时了,但是我让它作为一个很好的提醒。
请注意,Stream API并不总是可以替代。有些任务使用流更容易且更适合,有些则不是。对于您的情况,您还可以为此创建一个实用程序方法:
private static <T> List<List<T>> splitBySeparator(List<T> list, Predicate<? super T> predicate) {
final List<List<T>> finalList = new ArrayList<>();
int fromIndex = 0;
int toIndex = 0;
for(T elem : list) {
if(predicate.test(elem)) {
finalList.add(list.subList(fromIndex, toIndex));
fromIndex = toIndex + 1;
}
toIndex++;
}
if(fromIndex != toIndex) {
finalList.add(list.subList(fromIndex, toIndex));
}
return finalList;
}
并称它为List<List<String>> list = splitBySeparator(originalList, Objects::isNull);
。
可以改进以检查边缘情况。
尽管已经有几个答案,并且是一个可以接受的答案,但是该主题仍然缺少几点。首先,共识似乎是使用流解决此问题仅是一种练习,而传统的for循环方法更可取。其次,到目前为止给出的答案都忽略了使用数组或矢量样式技术的方法,我认为这大大改善了流解决方案。
首先,这是一个常规解决方案,用于讨论和分析:
static List<List<String>> splitConventional(List<String> input) {
List<List<String>> result = new ArrayList<>();
int prev = 0;
for (int cur = 0; cur < input.size(); cur++) {
if (input.get(cur) == null) {
result.add(input.subList(prev, cur));
prev = cur + 1;
}
}
result.add(input.subList(prev, input.size()));
return result;
}
这通常很简单,但是有些微妙。一点是,从prev
到的待处理子列表cur
始终处于打开状态。遇到时,null
我们将其关闭,将其添加到结果列表中,然后前进prev
。循环之后,我们无条件关闭子列表。
另一个观察结果是这是一个遍历索引的循环,而不是遍历值本身的循环,因此我们使用算术for循环而不是增强的“ for-each”循环。但是,这表明我们可以使用索引进行流式处理以生成子范围,而不是通过值进行流式处理并将逻辑放入收集器中(就像乔普·艾肯(Joop Eggen)提出的解决方案一样)。
意识到这一点之后,我们可以看到null
输入中的每个位置都是子列表的定界符:它是子列表的左端右端,它(加一个)是子列表的左端。对。如果我们能够处理极端情况,则可以找到一种方法,在其中找到null
元素出现的索引,将它们映射到子列表,然后收集子列表。
结果代码如下:
static List<List<String>> splitStream(List<String> input) {
int[] indexes = Stream.of(IntStream.of(-1),
IntStream.range(0, input.size())
.filter(i -> input.get(i) == null),
IntStream.of(input.size()))
.flatMapToInt(s -> s)
.toArray();
return IntStream.range(0, indexes.length-1)
.mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
.collect(toList());
}
获取发生索引null
很容易。绊脚石-1
在左侧和size
右侧添加。我选择使用Stream.of
进行附加,然后flatMapToInt
将其展平。(我尝试了其他几种方法,但这似乎是最干净的。)
在这里使用数组作为索引要方便一些。首先,与数组相比,访问数组的符号更好:indexes[i]
vs.indexes.get(i)
。其次,使用数组可避免装箱。
此时,数组中的每个索引值(最后一个除外)都比子列表的起始位置小一个。其直接右边的索引是子列表的末尾。我们只需在数组上流式传输并将每对索引映射到一个子列表中并收集输出。
讨论区
流方法比for循环版本略短,但密度更高。for循环版本很熟悉,因为我们一直在用Java来做这些事情,但是如果您还不知道该循环应该做什么,那么它就不那么明显了。在弄清楚prev
正在做什么以及为什么在循环结束后必须关闭打开子列表之前,可能必须模拟一些循环执行。(我最初忘记了它,但是在测试中发现了这一点。)
我认为,流方法更容易概念化正在发生的事情:获取一个指示子列表之间边界的列表(或数组)。那是一条轻松的两线客流。正如我上面提到的,困难在于找到一种将边缘值固定到两端的方法。如果这样做有更好的语法,例如,
// Java plus pidgin Scala
int[] indexes =
[-1] ++ IntStream.range(0, input.size())
.filter(i -> input.get(i) == null) ++ [input.size()];
它将使事情变得更加混乱。(我们真正需要的是数组或列表理解。)一旦有了索引,将它们映射到实际的子列表中并将它们收集到结果列表中就很简单了。
当并行运行时,这当然是安全的。
更新2016-02-06
这是创建子列表索引数组的一种更好的方法。它基于相同的原理,但是它会调整索引范围并为过滤器添加一些条件,以避免必须串联和平整索引。
static List<List<String>> splitStream(List<String> input) {
int sz = input.size();
int[] indexes =
IntStream.rangeClosed(-1, sz)
.filter(i -> i == -1 || i == sz || input.get(i) == null)
.toArray();
return IntStream.range(0, indexes.length-1)
.mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
.collect(toList());
}
更新2016-11-23
我在Devoxx Antwerp 2016上与Brian Goetz共同提出了一个主题为“并行思考”的视频(视频),其中介绍了此问题和我的解决方案。出现的问题是有一个细微的变化,它以“#”代替了null,但是在其他方面是相同的。在谈话中,我提到我针对此问题进行了许多单元测试。我在下面将它们作为独立程序附加到了我的循环和流实现中。对于读者来说,一个有趣的练习是针对我在此处提供的测试用例运行其他答案中提出的解决方案,并查看哪些失败以及为什么失败。(其他解决方案将不得不根据谓词进行拆分,而不是对null进行拆分。)
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.Arrays.asList;
public class ListSplitting {
static final Map<List<String>, List<List<String>>> TESTCASES = new LinkedHashMap<>();
static {
TESTCASES.put(asList(),
asList(asList()));
TESTCASES.put(asList("a", "b", "c"),
asList(asList("a", "b", "c")));
TESTCASES.put(asList("a", "b", "#", "c", "#", "d", "e"),
asList(asList("a", "b"), asList("c"), asList("d", "e")));
TESTCASES.put(asList("#"),
asList(asList(), asList()));
TESTCASES.put(asList("#", "a", "b"),
asList(asList(), asList("a", "b")));
TESTCASES.put(asList("a", "b", "#"),
asList(asList("a", "b"), asList()));
TESTCASES.put(asList("#"),
asList(asList(), asList()));
TESTCASES.put(asList("a", "#", "b"),
asList(asList("a"), asList("b")));
TESTCASES.put(asList("a", "#", "#", "b"),
asList(asList("a"), asList(), asList("b")));
TESTCASES.put(asList("a", "#", "#", "#", "b"),
asList(asList("a"), asList(), asList(), asList("b")));
}
static final Predicate<String> TESTPRED = "#"::equals;
static void testAll(BiFunction<List<String>, Predicate<String>, List<List<String>>> f) {
TESTCASES.forEach((input, expected) -> {
List<List<String>> actual = f.apply(input, TESTPRED);
System.out.println(input + " => " + expected);
if (!expected.equals(actual)) {
System.out.println(" ERROR: actual was " + actual);
}
});
}
static <T> List<List<T>> splitStream(List<T> input, Predicate<? super T> pred) {
int[] edges = IntStream.range(-1, input.size()+1)
.filter(i -> i == -1 || i == input.size() ||
pred.test(input.get(i)))
.toArray();
return IntStream.range(0, edges.length-1)
.mapToObj(k -> input.subList(edges[k]+1, edges[k+1]))
.collect(Collectors.toList());
}
static <T> List<List<T>> splitLoop(List<T> input, Predicate<? super T> pred) {
List<List<T>> result = new ArrayList<>();
int start = 0;
for (int cur = 0; cur < input.size(); cur++) {
if (pred.test(input.get(cur))) {
result.add(input.subList(start, cur));
start = cur + 1;
}
}
result.add(input.subList(start, input.size()));
return result;
}
public static void main(String[] args) {
System.out.println("===== Loop =====");
testAll(ListSplitting::splitLoop);
System.out.println("===== Stream =====");
testAll(ListSplitting::splitStream);
}
}
解决方法是使用Stream.collect
。解决方案是使用其构建器模式创建收集器。另一种选择是其他重载collect
稍微原始一些。
List<String> strings = Arrays.asList("a", "b", null, "c", null, "d", "e");
List<List<String>> groups = strings.stream()
.collect(() -> {
List<List<String>> list = new ArrayList<>();
list.add(new ArrayList<>());
return list;
},
(list, s) -> {
if (s == null) {
list.add(new ArrayList<>());
} else {
list.get(list.size() - 1).add(s);
}
},
(list1, list2) -> {
// Simple merging of partial sublists would
// introduce a false level-break at the beginning.
list1.get(list1.size() - 1).addAll(list2.remove(0));
list1.addAll(list2);
});
就像我看到的那样,我列出了一个字符串列表,其中总是至少有一个最后的(空)字符串列表。
带有累加器的解决方案:
正如@StuartMarks指出的那样,组合器无法完全满足并行协议。
由于@ArnaudDenoyelle的评论,使用的版本reduce
。
List<List<String>> groups = strings.stream()
.reduce(new ArrayList<List<String>>(),
(list, s) -> {
if (list.isEmpty()) {
list.add(new ArrayList<>());
}
if (s == null) {
list.add(new ArrayList<>());
} else {
list.get(list.size() - 1).add(s);
}
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>()))
null
,则null
可能导致累加器将一个空列表附加到左侧列表中。这样就保留了休息时间。
list.get(list.size() - 1).addAll(list2.remove(0));
应该是:list1.get(list1.size() - 1).addAll(list2.remove(0));
请不要投票。我没有足够的地方在评论中对此进行解释。
这是一个带有aStream
和a的解决方案,foreach
但这严格等同于Alexis的解决方案或一个foreach
循环(而且不太清楚,我无法摆脱copy构造函数):
List<List<String>> result = new ArrayList<>();
final List<String> current = new ArrayList<>();
list.stream().forEach(s -> {
if (s == null) {
result.add(new ArrayList<>(current));
current.clear();
} else {
current.add(s);
}
}
);
result.add(current);
System.out.println(result);
我知道您想找到一个使用Java 8的更优雅的解决方案,但我确实认为它不是为这种情况而设计的。正如汤普斯先生所说,在这种情况下,我们非常喜欢幼稚的方式。
尽管Marks Stuart的答案很简洁,直观且并行安全(也是最佳选择),但我想分享另一个有趣的解决方案,它不需要起点/终点边界技巧。
如果我们查看问题领域并考虑并行性,则可以使用分而治之的策略轻松解决此问题。不必将问题视为序列表,而要遍历,我们可以将问题看作是相同基本问题的组合:按null
值拆分列表。我们可以很容易直观地看到,我们可以使用以下递归策略来递归地解决问题:
split(L) :
- if (no null value found) -> return just the simple list
- else -> cut L around 'null' naming the resulting sublists L1 and L2
return split(L1) + split(L2)
在这种情况下,我们首先搜索任何null
值,然后立即找到一个值,我们立即剪切列表并在子列表上调用递归调用。如果找不到null
(基本情况),则完成此分支,然后返回列表。连接所有结果将返回我们正在搜索的列表。
一张图片胜过千言万语:
该算法既简单又完整:我们不需要任何特殊技巧就可以处理列表开头/结尾的边缘情况。我们不需要任何特殊技巧来处理边缘情况,例如空列表或仅包含null
值的列表。或以结尾null
或以开头的列表null
。
此策略的一个简单的简单实施如下所示:
public List<List<String>> split(List<String> input) {
OptionalInt index = IntStream.range(0, input.size())
.filter(i -> input.get(i) == null)
.findAny();
if (!index.isPresent())
return asList(input);
List<String> firstHalf = input.subList(0, index.getAsInt());
List<String> secondHalf = input.subList(index.getAsInt()+1, input.size());
return asList(firstHalf, secondHalf).stream()
.map(this::split)
.flatMap(List::stream)
.collect(toList());
}
我们首先搜索null
列表中任何值的索引。如果找不到,我们将返回列表。如果找到一个,则将列表分为2个子列表,在它们上进行流式处理,然后split
再次递归调用该方法。然后,提取子问题的结果列表,并将其合并为返回值。
请注意,可以很容易地使2个流成为parallel()并且由于问题的功能分解,该算法仍将起作用。
尽管代码已经非常简洁,但始终可以采用多种方式进行修改。举个例子,我们不用在基本情况下检查可选值,而是可以利用上的orElse
方法OptionalInt
返回列表的结束索引,从而使我们能够重用第二个流并额外过滤掉空列表:
public List<List<String>> split(List<String> input) {
int index = IntStream.range(0, input.size())
.filter(i -> input.get(i) == null)
.findAny().orElse(input.size());
return asList(input.subList(0, index), input.subList(index+1, input.size())).stream()
.map(this::split)
.flatMap(List::stream)
.filter(list -> !list.isEmpty())
.collect(toList());
}
该示例仅用于说明递归方法的简单性,适应性和优雅性。确实,此版本会带来很小的性能损失,并且如果输入为空(如果这样可能需要额外的空检查),则会失败。。
在这种情况下,递归可能不是最佳解决方案(用于查找索引的Stuart Marks算法仅是O(N),映射/拆分列表的开销很大),但是它使用简单,直观的可并行化算法来表达该解决方案,而无需任何操作副作用。
我不会深入研究复杂性和优点/缺点或具有停止条件和/或部分结果可用性的用例。我只是觉得有必要共享这种解决方案策略,因为其他方法仅仅是迭代的,或者使用了无法并行化的过于复杂的解决方案算法。
这是另一种使用分组功能的方法,该功能利用列表索引进行分组。
在这里,我将根据元素后跟的第一个索引对元素进行分组,并使用value null
。因此,在您的示例中,"a"
和"b"
将被映射到2
。另外,我正在将null
值映射到-1
索引,以后应将其删除。
List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");
Function<String, Integer> indexGroupingFunc = (str) -> {
if (str == null) {
return -1;
}
int index = list.indexOf(str) + 1;
while (index < list.size() && list.get(index) != null) {
index++;
}
return index;
};
Map<Integer, List<String>> grouped = list.stream()
.collect(Collectors.groupingBy(indexGroupingFunc));
grouped.remove(-1); // Remove null elements grouped under -1
System.out.println(grouped.values()); // [[a, b], [c], [d, e]]
您还可以null
通过将当前的最小索引缓存在中来避免每次获取element的第一个索引AtomicInteger
。更新Function
后将像:
AtomicInteger currentMinIndex = new AtomicInteger(-1);
Function<String, Integer> indexGroupingFunc = (str) -> {
if (str == null) {
return -1;
}
int index = names.indexOf(str) + 1;
if (currentMinIndex.get() > index) {
return currentMinIndex.get();
} else {
while (index < names.size() && names.get(index) != null) {
index++;
}
currentMinIndex.set(index);
return index;
}
};
好吧,经过一些工作,U提出了一种基于单行流的解决方案。最终使用它reduce()
来进行分组,这似乎是很自然的选择,但是List<List<String>>
通过reduce来将字符串转换为所需的字符串有点难看:
List<List<String>> result = list.stream()
.map(Arrays::asList)
.map(x -> new LinkedList<String>(x))
.map(Arrays::asList)
.map(x -> new LinkedList<List<String>>(x))
.reduce( (a, b) -> {
if (b.getFirst().get(0) == null)
a.add(new LinkedList<String>());
else
a.getLast().addAll(b.getFirst());
return a;}).get();
但是是1行!
当运行问题输入时,
System.out.println(result);
产生:
[[a, b], [c], [d, e]]
LinkedList<List<String>>
。我敢打赌这是一个难题。
这是一个非常有趣的问题。我想出了一种解决方案。它可能不是很出色,但是可以工作。
List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");
Collection<List<String>> cl = IntStream.range(0, list.size())
.filter(i -> list.get(i) != null).boxed()
.collect(Collectors.groupingBy(
i -> IntStream.range(0, i).filter(j -> list.get(j) == null).count(),
Collectors.mapping(i -> list.get(i), Collectors.toList()))
).values();
@Rohit Jain提出了类似的想法。我将空值之间的空间分组。如果您确实想要一个List<List<String>>
,可以附加:
List<List<String>> ll = cl.stream().collect(Collectors.toList());
这是AbacusUtil的代码
List<String> list = N.asList(null, null, "a", "b", null, "c", null, null, "d", "e");
Stream.of(list).splitIntoList(null, (e, any) -> e == null, null).filter(e -> e.get(0) != null).forEach(N::println);
声明:我是AbacusUtil的开发人员。
在我的StreamEx库中,有一种groupRuns
方法可以帮助您解决此问题:
List<String> input = Arrays.asList("a", "b", null, "c", null, "d", "e");
List<List<String>> result = StreamEx.of(input)
.groupRuns((a, b) -> a != null && b != null)
.remove(list -> list.get(0) == null).toList();
如果相邻元素对应该分组,则该groupRuns
方法BiPredicate
对相邻元素对返回true。之后,我们删除包含空值的组,并将其余的收集到列表中。
该解决方案是并行友好的:您也可以将其用于并行流。它也可以与任何流源一起使用(不仅像其他解决方案中的随机访问列表一样),而且它比基于收集器的解决方案要好一些,因为在这里您可以使用所需的任何终端操作,而不会浪费中间内存。
.filter(list -> list.get(0) != null)
如果您更喜欢,请使用。
使用String可以做到:
String s = ....;
String[] parts = s.split("sth");
如果所有顺序集合(因为String是一个字符序列)都具有此抽象,则这对于它们也是可行的:
List<T> l = ...
List<List<T>> parts = l.split(condition) (possibly with several overloaded variants)
如果我们将原始问题限制为字符串列表(并对其元素内容施加一些限制),我们可以这样修改它:
String als = Arrays.toString(new String[]{"a", "b", null, "c", null, "d", "e"});
String[] sa = als.substring(1, als.length() - 1).split("null, ");
List<List<String>> res = Stream.of(sa).map(s -> Arrays.asList(s.split(", "))).collect(Collectors.toList());
(但是请不要认真对待:)
否则,普通的旧递归也可以:
List<List<String>> part(List<String> input, List<List<String>> acc, List<String> cur, int i) {
if (i == input.size()) return acc;
if (input.get(i) != null) {
cur.add(input.get(i));
} else if (!cur.isEmpty()) {
acc.add(cur);
cur = new ArrayList<>();
}
return part(input, acc, cur, i + 1);
}
(请注意,在这种情况下,必须将null附加到输入列表中)
part(input, new ArrayList<>(), new ArrayList<>(), 0)
每当找到空值(或分隔符)时,按不同的令牌分组。我在这里使用了一个不同的整数(使用原子作为持有人)
然后重新映射生成的映射,以将其转换为列表列表。
AtomicInteger i = new AtomicInteger();
List<List<String>> x = Stream.of("A", "B", null, "C", "D", "E", null, "H", "K")
.collect(Collectors.groupingBy(s -> s == null ? i.incrementAndGet() : i.get()))
.entrySet().stream().map(e -> e.getValue().stream().filter(v -> v != null).collect(Collectors.toList()))
.collect(Collectors.toList());
System.out.println(x);
我正在看Stuart的“平行思考”视频。因此决定先解决问题,然后再在视频中看到他的回应。将随着时间更新解决方案。目前
Arrays.asList(IntStream.range(0, abc.size()-1).
filter(index -> abc.get(index).equals("#") ).
map(index -> (index)).toArray()).
stream().forEach( index -> {for (int i = 0; i < index.length; i++) {
if(sublist.size()==0){
sublist.add(new ArrayList<String>(abc.subList(0, index[i])));
}else{
sublist.add(new ArrayList<String>(abc.subList(index[i]-1, index[i])));
}
}
sublist.add(new ArrayList<String>(abc.subList(index[index.length-1]+1, abc.size())));
});
[list(v) for k, v in groupby(lst, key=lambda x: x is not None) if k]
。有Collectors.groupingBy
,但是老实说我不明白它是如何工作的……