Java 8中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作(bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错,但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言 + 多核时代综合影响的产物。
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。
原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后就用完了,就好比流水从面前流过,一去不复返。
和迭代器又不同的是,Stream 可以并行化操作,而迭代器只能命令式地、串行化操作。
当使用串行方式去遍历时,每个 item 读完后才会读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。
Stream 的另外一大特点是,数据源本身可以是无限的。
使用一个 Stream 通常包括三个基本步骤:
●①获取一个数据源
●②数据转换
●③执行操作获取想要的结果
每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。
●Collection.stream()
●Collection.parallelStream()
●Arrays.stream(T array) or Stream.of()
●java.io.BufferedReader.lines()
●java.util.stream.IntStream.range()
●java.nio.file.Files.walk()
●java.util.Spliterator
●Random.ints()
●BitSet.stream()
●Pattern.splitAsStream(java.lang.CharSequence)
●JarFile.stream()
一个 Stream 可以后面跟随零个或多个 intermediate 操作。其目的主要是打开 Stream,做出某种程度的数据映射或条件过滤,然后返回一个新的 Stream,交给下一个操作使用。这类操作都是惰性化的(lazy),也就是说,仅仅是调用到这类方法,并没有真正开始 Stream 的遍历。
一个 Stream 只能有一个 terminal 操作,当这个操作执行后,Stream 就被使用完了,无法再被操作。所以这必定是 Stream 的最后一个操作。Terminal 操作的执行,会真正开始 Stream 的遍历,并且会生成一个结果,或者一个 side effect(副作用)。
在对于一个Stream进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。
①对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
②对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个short-circuiting 操作是必要非充分条件。
Stream 中使用 Stage 的概念来描述一个完整的操作,并用某种实例化后的 PipelineHelper 来代表 Stage,将具有先后顺序的各个 Stage 连到一起,就构成了整个流水线。跟 Stream 相关类和接口的继承关系如图所示:
其中 IntPipeline、LongPipeline、DoublePipeline 这三个类是专门为三种基本类型(不是包装类型)而定制的,跟 ReferencePipeline 是并列关系,同时也含有 Head、StatelessOp 和 StatefulOp。Head 用于表示第一个 Stage,即调用诸如 Collection.stream() 方法产生的 Stage,这个 Stage 里不包含任何操作;StatelessOp 和 StatefulOp 分别表示无状态和有状态的 Stage,对应于无状态和有状态的中间操作。
通过 Collection.stream() 方法得到 Head 也就是 stage0,紧接着调用一系列的中间操作,不断产生新的 Stream。这些 Stream 对象以双向链表的形式组织在一起,构成整个流水线,由于每个 Stage 都记录了前一个 Stage 和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是 Stream 记录操作的方式。
要想让流水线起到应有的作用,需要一种将所有操作叠加到一起的方案。由于前面的 Stage 并不知道后面 Stage 到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前 Stage 本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻 Stage 之间的调用关系。
这种协议由 Sink 接口完成,Sink 接口包含的主要方法如下表所示:
有了上面的协议,相邻 Stage 之间调用就很方便了,每个 Stage 都会将自己的操作封装到一个 Sink 里,前一个 Stage 只需调用后一个 Stage 的 accept() 方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink 的 begin() 和 end() 方法也是必须实现的。
Sink 的四个接口方法常常相互协作,共同完成计算任务。实际上 Stream API 内部实现的的本质,就是如何重载 Sink 的这四个主要接口方法。
有了 Sink 对操作的包装,Stage 之间的调用问题就解决了,执行时只需要从流水线的 head 开始对数据源依次调用每个 Stage 对应的 Sink.{begin()、accept()、cancellationRequested()、end()}方法就可以了。
一种可能的 Sink.accept() 方法流程是这样的:
●首先使用当前 Sink 包装的回调函数处理入参。
●然后将处理结果传递给流水线下游的 Sink。
Sink 接口的其他几个方法也是按照这种“处理->转发”的模型实现。
下面,结合一个具体例子看看 Stream 的中间操作是如何将自身的操作包装成 Sink 以及 Sink 是如何将处理结果转发给下一个 Sink 的。
●先看 Stream.map() 方法:
// 代码位于ReferencePipeline中,调用该方法将产生一个新的Stream
@Override
@SuppressWarnings("unchecked")
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
Objects.requireNonNull(mapper);
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
// opWripSink()方法返回由回调函数包装而成Sink
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
// 使用当前Sink包装的回调函数mapper处理u,将处理结果传递给流水线下游的Sink
downstream.accept(mapper.apply(u));
}
};
}
};
}
代码逻辑很简单,就是将回调函数 mapper 包装到一个 Sink 当中。由于 Stream.map() 是一个无状态的中间操作,所以 map() 方法返回了一个 StatelessOp 内部类对象(一个新的Stream),调用这个新 Stream 的 opWripSink() 方法将得到一个包装了当前回调函数的 Sink。
●再来看一个复杂一点的例子,Stream.sorted() 方法:
Stream.sorted() 方法将对 Stream 中的元素进行排序,显然这是一个有状态的中间操作,因为读取所有元素之前是没法得到最终顺序的。
sorted() 一种可能封装的 Sink 代码如下:
// 代码位于SortedOps中
private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
// 存放用于排序的元素
private ArrayList<T> list;
RefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
super(sink, comparator);
}
// 创建一个存放排序元素的列表
@Override
public void begin(long size) {
if (size >= Nodes.MAX_ARRAY_SIZE)
throw new IllegalArgumentException(Nodes.BAD_SIZE);
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
@Override
public void end() {
// 只有元素全部接收之后才能开始排序
list.sort(comparator);
downstream.begin(list.size());
// 下游Sink不包含短路操作
if (!cancellationWasRequested) {
// 将处理结果传递给流水线下游的Sink
list.forEach(downstream::accept);
}
// 下游Sink包含短路操作
else {
for (T t : list) {
// 每次都调用cancellationRequested()询问是否可以结束处理
if (downstream.cancellationRequested()) break;
// 将处理结果传递给流水线下游的Sink
downstream.accept(t);
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
// 使用当前Sink包装动作处理t(只是简单的将元素添加到中间列表当中)
list.add(t);
}
}
上述代码完美的展现了 Sink 的四个主要方法是如何协同工作的:
●首先 beging() 方法告诉 Sink 参与排序的元素个数,方便确定中间结果容器的的大小。
●之后通过 accept() 方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素。
●最后 end() 方法告诉 Sink 所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的 Sink。
●如果下游的 Sink 是短路操作,将结果传递给下游时不断询问下游 cancellationRequested() 是否可以结束处理。
Sink 完美封装了 Stream 每一步操作,并给出了“处理->转发”的模式来叠加操作,就差最后一步启动执行。如何启动这一连串的操作呢?启动的触发器就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
结束操作之后不能再有别的操作,所以结束操作不会创建新的 Stage,直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的 Sink,这也是流水线中最后一个 Sink,这个 Sink 只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)。对于 Sink 的“处理->转发”模型,结束操作的 Sink 就是调用链的出口。
再来看下上游的 Sink 是如何找到下游 Sink 的。Stream 类库的设计者设置了一个 Sink AbstractPipeline.opWrapSink(int flags, Sink downstream) 方法来得到 Sink,该方法的作用是返回一个新的包含了当前 Stage 代表的操作以及能够将结果传递给 downstream 的 Sink 对象。
为什么要产生一个新对象而不是返回一个 Sink 字段?这是因为使用 opWrapSink() 可以将当前操作与下游 Sink(上文中的 downstream 参数)结合成新 Sink。试想只要从流水线的最后一个 Stage 开始,不断调用上一个 Stage 的 opWrapSink() 方法直到最开始(不包括 stage0,因为 stage0 代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的 Sink,用代码表示就是这样:
// 代码位于AbstractPipeline中
@Override
@SuppressWarnings("unchecked")
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
Objects.requireNonNull(sink);
// 从下游向上游不断包装Sink。如果到达最初传入的sink代表结束操作。
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
// 函数返回时就可以得到一个代表了流水线上所有操作的Sink
return (Sink<P_IN>) sink;
}
现在流水线上从开始到结束的所有的操作都被包装到了一个 Sink 里,执行这个 Sink 就相当于执行整个流水线,执行 Sink 的代码如下:
// 代码位于AbstractPipeline中
// 对spliterator代表的数据执行wrappedSink代表的操作
@Override
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
Objects.requireNonNull(wrappedSink);
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
// 通知开始遍历
wrappedSink.begin(spliterator.getExactSizeIfKnown());
// 遍历
spliterator.forEachRemaining(wrappedSink);
// 通知遍历结束
wrappedSink.end();
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}
上述代码首先调用 wrappedSink.begin() 方法告诉 Sink 数据即将到来,然后调用 spliterator.forEachRemaining() 方法对数据进行遍历(Spliterator 是容器的一种迭代器),最后调用 wrappedSink.end() 方法通知 Sink 数据处理结束。
流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里呢?
首先要说明的是不是所有的 Stream 结束操作都需要返回结果,有些操作只是为了使用其副作用(Side-effects),比如使用 Stream.forEach() 方法将结果打印出来就是常见的使用副作用的场景(事实上,除了打印之外其他场景都应避免使用副作用),对于真正需要返回结果的结束操作结果存在哪里呢?
特别说明:副作用不应该被滥用,也许在 Stream.forEach() 里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为 Stream 可能会并行执行。大多数使用副作用的地方都可以使用归约操作更安全和有效的完成。
// 错误的收集方式
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s)); // Unnecessary use of side-effects!
// 正确的收集方式
List<String>results =
stream.filter(s -> pattern.matcher(s).matches())
.collect(Collectors.toList()); // No side-effects!
回到流水线执行结果的问题上来,需要返回结果的流水线结果存在哪里呢?这要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作:
●对于表中返回 boolean 或者 Optional 的操作,由于只返回一个值,只需要在对应的 Sink 中记录这个值,等到执行结束时返回就可以了。
●对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect()、reduce()、max()、min() 都是归约操作,虽然 max() 和 min() 也是返回一个 Optional,但事实上底层是通过调用 reduce() 方法实现的。
●对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做 Node 的数据结构中的。Node 是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。
Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。
Stream stream = Stream.of("a", "b", "c");
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
List<String> list = Arrays.asList(strArray);
stream = list.stream();
对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
// 不输出3
IntStream.range(1, 3).forEach(System.out::println);
// 输出3
IntStream.rangeClosed(1, 3).forEach(System.out::println);
// Array
String[] strArray = stream.toArray(String[]::new);
// Collection
List<String> list = stream.collect(Collectors.toList());
List<String> list = stream.collect(Collectors.toCollection(ArrayList::new));
Set set = stream.collect(Collectors.toSet());
Stack stack = stream.collect(Collectors.toCollection(Stack::new));
// String
String str = stream.collect(Collectors.joining()).toString();
注意:一个 stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。
当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下:
●Intermediate:map(mapToInt、flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。
●Terminal:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。
●Short-circuiting:anyMatch、allMatch、noneMatch、findFirst、findAny、limit。
List<String> list = Arrays.asList("A", "B", "C", "D", "E");
// 方式一:JDK1.8之前的循环方式
for (String item: list) {
System.err.println(item);
}
// 方式二:使用Stream的forEach方法
list.stream().forEach(item -> System.err.println(item));
// 方式三:方式二的简化方式
// 由于方法引用也属于函数式接口,所以方式二中的Lambda表达式也可以使用方法引用来代替
list.stream().forEach(System.err::println);
// 进一步简化可以写为
list.forEach(System.err::println);
forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,无法对一个 Stream 进行两次 terminal 运算。
如果有类似重复使用需求,可以使用具有相似功能的 intermediate 操作 peek。
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.err.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.err.println("Mapped value: " + e))
.collect(Collectors.toList());
forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。
List<Person> people = Arrays.asList(
Person.builder().name("A").age(28).build(),
Person.builder().name("B").age(18).build(),
Person.builder().name("C").age(17).build()
);
people.stream().filter(p -> p.getAge() > 18).forEach(System.err::println);
filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。
List<String> list = Arrays.asList("A", "B", "C", "D", "E");
// 转换小写
list.stream().map(String::toLowerCase).forEach(System.err::println);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
// 转换为平方数
nums.stream().map(n -> n * n).forEach(System.err::println);
map 生成的是个一对一的映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。
List<Integer> a = Arrays.asList(1, 2, 3);
List<Integer> b = Arrays.asList(4, 5, 6);
List<List<Integer>> collect = Stream.of(a, b).collect(Collectors.toList());
// [[1, 2, 3], [4, 5, 6]]
System.err.println(collect);
// 将多个集合中的元素合并到一个集合
List<Integer> mergeList = Stream.of(a, b).flatMap(Collection::stream).collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6]
System.err.println(mergeList);
flatMap 把 Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起。
List<String> list = Arrays.asList("c", "e", "a", "d", "b");
list.stream().sorted((s1, s2) -> s1.compareTo(s2)).forEach(System.out::println);
// 或者简写为
list.stream().sorted(String::compareTo).forEach(System.err::println);
对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
Stream<String> stream = Stream.of("A", "B", "C", "D", "A", "C");
stream.distinct().forEach(System.err::println);
Stream<String> stream = Stream.of("A", "B", "C", "D", "A", "C");
long count = stream.count();
System.err.println(count);
List<String> list = Arrays.asList("1", "2", "3", "4", "5");
Optional<String> optional = list.stream().min(String::compareTo);
System.err.println(optional.get());
min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。
List<String> skipList = Arrays.asList("a", "b", "c", "d", "e");
// c d e
skipList.stream().skip(2).forEach(System.err::println);
List<String> limitList = Arrays.asList("a", "b", "c", "d", "e");
// a b c
limitList.stream().limit(3).forEach(System.err::println);
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。
对一个 parallel 的 Steam 管道来说,如果其元素是有序的,那么 limit 操作的成本会比较大,因为它的返回对象必须是前 n 个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用 parallel Stream。
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
List<String> collect = list.stream().collect(Collectors.toList());
Object[] objects = list.stream().toArray();
List<String> list1 = Arrays.asList("a", "b");
List<String> list2 = Arrays.asList("c", "d");
Stream<String> concatStream = Stream.concat(list1.stream(), list2.stream());
concatStream.forEach(System.err::println);
List<String> list = Arrays.asList("A", "B", "C", "D", "E");
// parallelStream可以并行计算,速度比stream更快
boolean result = list.parallelStream().anyMatch(item -> item.equals("C"));
System.err.println(result);
Stream 有三个 match 方法:
●allMatch:Stream 中全部元素符合传入的 predicate,返回 true。
●anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true。
●noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true。
它们都不是要遍历全部元素才返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。
// 字符串连接
Optional<String> optional = Stream.of("A", "B", "C", "D", "E").reduce((before, after) -> before + "," + after);
optional.ifPresent(System.err::println);
List<BigDecimal> list = Arrays.asList(
new BigDecimal("11.11"),
new BigDecimal("22.22"),
new BigDecimal("33.33")
);
// 求和,有起始值
BigDecimal sum1 = list.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
System.err.println(sum1);
// 求和,无起始值
BigDecimal sum2 = list.stream().reduce(BigDecimal::add).get();
System.err.println(sum2);
// 求最小值
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
System.err.println(minValue);
// 过滤后字符串连接
String reduce = Stream.of("a", "B", "c", "D", "e", "F")
.filter(x -> x.compareTo("Z") > 0)
.reduce("", String::concat);
System.err.println(reduce);
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。
System.err.println(Stream.of("A", "B", "C", "D", "E").findFirst().get());
System.err.println(Stream.of("A", "B", "C", "D", "E").findAny().get());
这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。(它的返回值类型为 Optional)
// 生成 10 个随机整数
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.err::println);
// 另一种方法
IntStream.generate(() -> (int) (System.nanoTime() % 100)).limit(10).forEach(System.err::println);
通过实现 Supplier 接口,可以自己控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。
Stream.generate() 还允许用户自己实现 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。
private class PersonSupplier implements Supplier<Person> {
private long index = 0;
private final Random random = new Random();
@Override
public Person get() {
return Person.builder().num(index++).name("A" + index).age(random.nextInt(100)).build();
}
}
// 自实现 Supplier
Stream.generate(new PersonSupplier()).limit(10).forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));
// 生成等差数列
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.err.print(x + " "));
iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。
// 按照年龄分组
Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).limit(100).collect(Collectors.groupingBy(Person::getAge));
for (Map.Entry<Integer, List<Person>> entry : personGroups.entrySet()) {
System.err.println("Age " + entry.getKey() + " = " + entry.getValue().size());
}
// 按照未成年人和成年人分组
Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).limit(100).collect(Collectors.partitioningBy(p -> p.getAge() < 18));
System.out.println("Children number: " + children.get(true).size());
System.out.println("Adult number: " + children.get(false).size());
partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。