尽管已经有几个答案并且有一个被接受的答案,但这个主题仍然缺少一些要点。首先,共识似乎是使用流解决此问题只是一种练习,传统的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's proposed solution所做的那样)。
一旦我们意识到了这一点,我们就可以看到输入中每个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
进行平铺。(我尝试了几种其他方法,但这种方法似乎最清晰。)
在这里使用数组更加方便。首先,访问数组的符号比 List 更好:
indexes[i]
vs.
indexes.get(i)
。其次,使用数组避免了装箱。
此时,数组中的每个索引值(除了最后一个)都比子列表的开始位置少一。它右边的索引是子列表的结尾。我们只需要在数组上进行流处理,将每对索引映射到子列表并收集输出即可。
讨论
流式编程方法比for循环版本稍微短一些,但更加密集。for循环版本很熟悉,因为我们经常在Java中使用这种方式,但如果您不知道这个循环应该做什么,它并不明显。在弄清楚prev的作用以及为什么必须在循环结束后关闭开放子列表之前,您可能需要模拟几次循环执行。(我最初忘记了它,但我在测试中发现了这一点。)
我认为流式编程方法更容易理解正在发生的事情:获取一个指示子列表之间边界的列表(或数组)。这是一个简单的两行流式处理代码。如上所述,困难在于找到一种将边缘值附加到末尾的方法。如果有更好的语法来完成此操作,例如:
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日
我与Brian Goetz一起在Devoxx Antwerp 2016上做了一个演讲,题目是“Thinking In Parallel”(video),其中涉及到了这个问题以及我的解决方案。在那里提出的问题略有不同,是以“#”为分割符而不是空值,但本质相同。在演讲中,我提到我为这个问题编写了大量单元测试。我将它们作为一个独立的程序附在下面,还有我的循环和流实现。读者可以尝试用其他答案提供的解决方案运行这些测试用例,并查看哪些失败了以及原因。(其他解决方案必须根据谓词进行适应,而不是基于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);
}
}
Collectors.groupingBy
与 Python 的itertools.groupby
工作方式非常不同,更像是Collectors.partitioningBy
,只是有超过两个组,即它会将所有非空字符串放在一个桶中。 - tobias_k