文章目录
Stream流
1、认识Stream流(源码说明)
1.1、Stream流和Collection的区别
在Package java.util.stream这个包中引入的关键类是stream。
类Stream、IntStream、LongStream和DoubleStream是对象上的流以及原始int、long和double类型。流在几个方面与集合不同:
- 没有存储。
流不是存储元素的数据结构;相反,它通过计算操作管道传递来自源的元素,如数据结构、数组、生成器函数或I/O通道。 - 函数式编程、功能在本质上。
流上的操作生成结果,但不修改其源。例如,过滤从集合获得的流将生成一个没有过滤元素的新流,而不是从源集合中删除元素。 - 懒惰式查找。
许多流操作,如过滤、映射或重复删除,都可以惰性地实现,从而暴露了优化的机会。例如,“查找第一个有三个连续元音字母的字符串”不需要检查所有输入字符串。流操作可分为中间(流生产)操作和终端(价值或副作用生产)操作。中间操作总是惰性的。 - 流可能是无限的。
虽然集合具有有限的大小,但流不需要。短路操作,如limit(n)
或findFirst()
,允许在有限的时间内完成对无限流的计算。 - 消耗品。在流的生命周期中,流的元素只被访问一次。与迭代器一样,必须生成新的流来重新访问源的相同元素。被访问过的流会关闭
1.2、流的获取方式
流可以通过多种方式获得。一些例子包括:
- 通过stream()和parallelStream()方法获取一个集合;
- 通过array .stream(Object[]);
- 从流类上的静态工厂方法,例如stream .of(Object[]), IntStream。范围(int, int)或流。迭代(对象,UnaryOperator);
- 一个文件的行可以从BufferedReader.lines();
- 文件路径流可以从文件中的方法获得;
- 可以从random .ints()中获得随机数流;
- JDK中许多其他与流兼容的方法,包括BitSet.stream()、Pattern.splitAsStream(java.lang.CharSequence)和JarFile.stream()。
使用这些技术的第三方库可以提供其他流源。
1.3、流操作和管道
流操作和管道流作业分为中间作业和终端作业,并结合起来形成流管道。
流管道由源(如集合、数组、生成器函数或I/O通道)组成;然后是零个或多个中间操作和终端操作
- 中间操作如Stream、过滤器、Stream.map;
- 终端操作,如Stream、forEach或Stream.reduce。
中间操作
中间操作返回一个新的流。他们总是很懒;执行filter()之类的中间操作实际上并不执行任何过滤,而是创建一个新的流,在遍历该流时,该流包含与给定谓词匹配的初始流的元素。在执行管道的终端操作之前,不会开始对管道源的遍历。
终端操作
终端操作,如Stream。forEach或IntStream。可以遍历流以产生结果或副作用。终端操作完成后,认为流管道已被消耗,不能再使用;如果需要再次遍历相同的数据源,则必须返回数据源以获得新的流。在几乎所有情况下,终端操作都是即时的,在返回之前完成它们对数据源的遍历和对管道的处理。只有终端操作iterator()和spliterator()不是;
当现有操作不足以完成任务时,它们作为“逃生舱口”提供,以支持任意的客户机控制的管道遍历。延迟处理流考虑到显著的效率;在诸如上面的过滤器-映射-和示例这样的管道中,过滤、映射和和可以融合为对数据的一次传递,中间状态最小。懒惰还可以避免在不必要的时候检查所有的数据;对于“查找第一个超过1000个字符的字符串”这样的操作,只需要检查足够多的字符串来查找具有所需特征的字符串,而不需要检查源中的所有可用字符串。(当输入流是无限的而不仅仅是很大的时候,这种行为就变得更加重要了。)
中间操作进一步分为无状态操作和有状态操作。无状态操作(如filter和map)在处理新元素时不会保留以前看到的元素的状态——每个元素都可以独立于对其他元素的操作进行处理。有状态操作(如distinct和排序)在处理新元素时可以合并以前看到的元素的状态。
- 有状态操作可能需要在产生结果
之前
处理整个输入。例如,在看到流的所有元素之前,无法从对流的排序中产生任何结果。因此,在并行计算下,一些包含有状态中间操作的管道可能需要对数据进行多次传递,或者可能需要缓冲重要数据。
- 只包含无状态中间操作的管道可以
一次性处理(无论是顺序的还是并行的),使用最小的数据缓冲。
此外,一些操作被认为是短路操作。一个中间操作是短路的,当有无限输入时,它可能会产生一个有限的流。当一个终端操作出现无限输入时,它可能在有限时间内结束,这就是短路。管道中有短路操作是对无限流的处理在有限时间内正常终止的必要条件,但不是充分条件。
1.4、并行性
并行性使用显式for循环处理元素本质上是串行的。流将计算重新定义为聚合操作的管道,而不是对每个单独元素的命令式操作,从而促进并行执行。所有的流操作既可以串行执行,也可以并行执行。JDK中的流实现创建串行流,除非显式请求并行性。例如,Collection拥有方法Collection.stream()
和Collection.parallelstream()
,它们分别生成顺序流和并行流;其他含流方法,如流入法。range(int,int)
产生顺序流,但是可以通过调用它们的BaseStream.parallel()
方法来有效地并行这些流。要并行执行前面的“widgets权重总和”查询,我们可以这样做:
int sumOfWeights = widgets.parallelStream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
这个示例的串行版本和并行版本之间的唯一区别是使用“parallelStream()
”而不是“stream()
”创建初始流。流管道是顺序执行还是并行执行,这取决于调用终端操作的流的模式。流的顺序或并行模式可以通过BaseStream.isParallel()方法确定,流的模式可以通过BaseStream.sequential()和BaseStream.parallel()操作修改。
最近的顺序或并行模式设置适用于整个流管道的执行。除了标识为显式非确定性的操作(如findAny()
)外,流是按顺序执行还是并行执行都不应改变计算结果。
大多数流操作接受描述用户指定行为的参数,这些参数通常是lambda表达式
。为了保持正确的行为,这些行为参数必须是无干扰的,并且在大多数情况下必须是无状态的。这些参数总是函数接口的实例,比如Function,通常是lambda表达式或方法引用。
流是可以并行执行的,当流中存在大量元素时,可以显著提升性能。并行流底层使用的ForkJoinPool, 它由
ForkJoinPool.commonPool()
方法提供。底层线程池的大小最多为五个 - 具体取决于 CPU 可用核心数
1.5、不干扰内政
流使您能够在各种数据源上执行可能并行的聚合操作,甚至包括非线程安全的集合,如ArrayList
。只有在流管道执行期间能够防止对数据源的干扰,这才有可能实现。除了escape-hatch操作iterator()和spliterator()之外,执行在终端操作被调用时开始,在终端操作完成时结束。对于大多数数据源,防止干扰意味着确保数据源在流管道执行期间完全没有被修改。
值得注意的例外是其源为并发集合的流,这些集合是专门设计来处理并发修改的。并发流源是指Spliterator报告并发特性的流源。
因此,流管道中,源可能不是并发的行为参数不应该修改流的数据源。如果一个行为参数修改或导致修改流的数据源,那么它就会干扰非并发数据源。不干涉的需求适用于所有管道,而不仅仅是并行管道。
除非流源是并发的,否则在流管道执行期间修改流的数据源会导致异常、不正确的答案或不一致的行为。对于行为良好的流源,可以在终端操作开始之前对源进行修改,这些修改将反映在被覆盖的元素中。例如,考虑以下代码:
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
l.add("three");
String s = sl.collect(joining(" "));
首先创建一个由两个字符串组成的列表,然后从该列表创建一个流。接下来,通过添加第三个字符串“three”来修改列表。最后,流中的元素被收集并连接在一起。由于列表在终端收集操作开始之前被修改
,结果将是一个“onetwothree”的字符串。
所有从JDK集合返回的流,以及大多数其他JDK类,在这种方式下都表现良好;对于由其他库生成的流,请参阅构建行为良好的流的需求的低级流构造。
1.6、无状态行为
如果流操作的行为参数是有状态的,则流管道结果可能是不确定的或不正确的。有状态lambda(或其他实现适当函数接口的对象)的结果取决于在流管道执行期间可能改变的任何状态。有状态lambda的一个例子是映射()的参数:
Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...
还请注意,试图从行为参数访问可变状态会给您带来一个关于安全和性能的糟糕选择;如果不同步访问该状态,就会出现数据竞争,从而导致代码崩溃,但是如果同步访问该状态,就会有竞争破坏并行性的风险,而并行性正是您想要从中获益的。最好的方法是避免将有状态的行为参数完全流化操作;通常有一种方法可以重构流管道以避免有状态性。
1.7、副作用
通常,流操作的行为参数的副作用是不鼓励的,因为它们经常会无意间违反无状态需求,以及其他线程安全风险。如果行为参数确实有副作用,除非明确说明,否则不能保证:
- 那些副作用对其他线程的可见性;
- 同一流管道中对“同一”元素的不同操作在同一线程中执行;
- 行为参数总是被调用,因为流实现可以自由地从流管道省略操作(或整个阶段),如果它能证明它不会影响计算的结果。
副作用的顺序可能令人吃惊。即使当管道被限制产生与流源相遇顺序一致的结果时(例如,IntStream.range(0,5).parallel()。map(x->toarray()必须生成[0,2,4,6,8])
,不能保证mapper函数应用到单个元素的顺序,也不能保证给定元素的行为参数是在哪个线程中执行的。
副作用的避免也令人惊讶。除了终端操作forEach和forEachOrdered之外,当流实现可以在不影响计算结果的情况下优化掉行为参数的执行时,行为参数的副作用可能并不总是被执行。(有关特定的示例,请参阅在计数操作中记录的API注释。)
人们可能会尝试使用副作用的许多计算可以更安全、更有效地表达,而没有副作用,例如使用减少而不是可变累加器。但是,使用println()进行调试之类的副作用通常是无害的
。少量流操作,如forEach()和peek(),只能通过副作用进行操作;这些东西应该小心使用。作为如何将不适当使用副作用的流管道转换为不使用副作用的流管道的示例,下面的代码在字符串流中搜索与给定正则表达式匹配的字符串,并将匹配项放在一个列表中。
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s));//不必要的使用副作用!
这段代码不必要地使用了副作用。如果并行执行,ArrayList的非线程安全性将导致不正确的结果,而添加所需的同步将导致争用,从而破坏并行的好处。此外,在这里使用副作用是完全没有必要的;forEach()可以简单地用一个更安全、更高效、更适合并行化的reduce操作替换:
List<String>results = stream.filter(s -> pattern.matcher(s).matches()) .collect(Collectors.toList()); // No side-effects!/ /没有副作用!
1.8、排序
流可能有也可能没有定义的相遇顺序。流是否具有相遇顺序取决于源操作和中间操作。某些流源(如列表或数组)本质上是有序的,而其他流源(如HashSet)则不是有序的。一些中间操作,比如sorts(),可能会在原本无序的流上强加相遇顺序,而其他操作则可能呈现无序的有序流,比如BaseStream.unordered()。此外,一些终端操作可能忽略遇到顺序,比如forEach()。
如果一个流是有序的,那么大多数操作都被限制为对其相遇顺序中的元素进行操作;如果流的源是一个包含[1,2,3]的列表,则执行map(x->gt;x*2)一定是[2,4,6]
。但是,如果源没有定义相遇顺序,那么值[2,4,6]的任何排列都是有效的结果。
对于顺序流,是否存在相遇顺序不影响性能,只影响确定性。如果一个流是有序的,那么在相同的源上重复执行相同的流管道将产生相同的结果;如果没有排序,重复执行可能会产生不同的结果。
对于并行流,放松顺序约束有时可以实现更高效的执行。如果元素的排序不相关,可以更有效地实现某些聚合操作,比如过滤重复项(distinct())或分组缩减(collections . groupingby())。类似地,本质上与顺序相关联的操作(如limit())可能需要进行缓冲以确保正确的顺序,从而破坏了并行性的好处。如果流有一个相遇顺序,但用户并不特别关心这个相遇顺序,那么使用unordered()显式地对流进行反排序可能会提高某些有状态或终端操作的并行性能。然而,大多数流管道,例如上面的“块的重量总和”示例,仍然可以在排序约束下有效地并行化。
1.9、缩减操作
缩减操作(也称为折叠):接受一组输入元素,并通过重复应用组合操作(如查找一组数字的总和或最大值,或将元素累积到一个列表)将它们组合成一个汇总结果。
streams类具有多种形式的一般约简操作
- reduce()和collect()
- 以及多种专门化的约简形式,如sum()、max()或count()。
当然,这些操作可以很容易地实现为简单的顺序循环,如:
int sum = 0;
for (int x : numbers) {
sum += x;
}
但是,有很好的理由选择reduce操作而不是如上所述的突变累积操作。reduce不仅“更抽象”——它作为一个整体而不是单个元素对流进行操作——而且只要用于处理元素的函数是关联的和无状态的,一个正确构造的reduce操作本质上是可并行的。例如,给定一串我们想求其和的数字,我们可以写:
int sum = numbers.stream().reduce(0, (x,y) -> x+y); //lambda表达式
or:
int sum = numbers.stream().reduce(0, Integer::sum);//方法引用
这些减少操作可以运行安全并行几乎没有修改:
int sum = numbers.parallelStream().reduce(0, Integer::sum);
约简并行性很好,因为实现可以并行地对数据的子集进行操作,然后结合中间结果得到最终的正确答案。(即使该语言具有“parallel for-each”结构,变化的积累方法仍然需要开发人员对共享的积累变量sum提供线程安全的更新,而所需的同步则可能会消除并行带来的任何性能增益。)
使用reduce()可以消除并行化reduce操作的所有负担,并且库可以提供一个高效的并行实现,而不需要额外的同步。
前面展示的“小部件”示例展示了reduce与其他操作如何结合使用大容量操作替换for循环。如果小部件是一个小部件对象的集合,它有一个getWeight方法,我们可以通过:
OptionalInt heaviest = widgets.parallelStream()
.mapToInt(Widget::getWeight)
.max();
在其更一般的形式中,对类型元素的一种简化操作。生成类型<u>
的结果需要三个参数:
U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator combiner);
在这里,identity元素既是减少的初始种子值,也是没有输入元素时的默认结果。accumulator函数接受一个部分结果和下一个元素,并生成一个新的部分结果。组合函数结合两个部分结果产生一个新的部分结果。(在对输入进行分块,对每个分块计算部分累加,然后对部分结果进行组合以产生最终结果的并行压缩中,合成器是必不可少的。)
更正式地说,恒等值必须是合成器函数的恒等值。这意味着对于所有的u,组合。应用(identity, u)等于u。另外,合成器函数必须是结合的,并且必须与累加器函数兼容:对于所有的u和t,合成器。应用(u,蓄电池。对累加器应用(identity, t))必须是equals()。应用(u, t)。
三参数形式是两参数形式的一般化,将映射步骤合并到积累步骤中。我们可以使用更一般的形式重新定义简单的权值和示例,如下所示:
int sumOfWeights = widgets.stream()
.reduce(0,
(sum, b) -> sum + b.getWeight(),
Integer::sum);
尽管显式的map-reduce形式可读性更强,因此通常应该是首选。广义形式提供的情况,其中重要的工作可以优化,通过结合映射和减少到一个单一的函数
2、简单使用Stream来进行
2.1、初次使用
要求:打印姓为张、长度为3的string
package Stream流AndMethodFerence;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
public class Stream01_What {
public static void main(String[] args) {
Set<String> stringSet = new HashSet<>();
stringSet.add("张三");
stringSet.add("李四");
stringSet.add("王五");
stringSet.add("田七");
stringSet.add("张三枫");
stringSet.add("张国荣");
//利用集合操作,打印姓为张、长度为3的string
/* for (String s:stringSet) {
if(s.length()==3&&s.startsWith("张")){
System.out.println(s);
}
}*/
/* //利用stream流操作:
Stream<String> stream = stringSet.stream();
stream
.filter((name)->name.startsWith("张"))
.filter((name)->name.length()==3)
.forEach((name)-> System.out.println(name));*/
Stream<String> stream = stringSet.stream();
stream
.filter((name)->name.startsWith("张"))
.filter((name)->name.length()==3)
.forEach(System.out::println);
}
}
我们上面所使用的filter就是中间操作,每用一次得到一个新的stream流。forEach则是终端操作。在该操作之前,上面的中间操作,即过滤是还没有进行的。而在终端操作之后,stream流关闭。我们不能再使用该stream流,如在上面再加一行
stream.forEach(System.out::println);
为了克服这个限制,我们可以为我们想要执行的每个终端操作创建一个新的流链,例如,我们可以通过 Supplier 来包装一下流,通过 get() 方法来构建一个新的 Stream 流,如下所示:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c");
通过构造一个新的流,来避开流不能被复用的限制, 这也是取巧的一种方式。如下:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c");
Stream<String> stringStream = streamSupplier.get();
stringStream.forEach((s)-> System.out.println(s));
stringStream = streamSupplier.get();
stringStream.forEach((s)-> System.out.println(s));
2.2、中间操作使用
上面提到了中间操作是只有存在终端操作执行时才会执行。进而实现了性能上的提升。具体例子如:
Stream.of("a1","b2","c3","d4","e5")
.map((s)->{
System.out.println("map: "+s);
return s.toUpperCase();//变为大写
}).filter((s)-> {
System.out.println("filter: "+s);
return s.startsWith("C");//过滤,c开头
}).forEach(s->System.out.println("foreach--"+s));
中间操作,流水线处理,进而打印出每个字符。但是因为是中间操作,过滤行为只发生于调用了forEach方法,打印C3。我们可以看到我们只需要执行的操作:是打印C3
下面我们改变以下顺序:先filter再执行map
Stream.of("a1","b2","c3","d4","e5")
.filter((s)-> {
System.out.println("filter2: "+s);
return s.startsWith("c");//过滤,c开头
}) .map((s)->{
System.out.println("map2: "+s);
return s.toUpperCase();//变为大写
}).forEach(s->System.out.println("foreach2--"+s));
可以从上面的输出上看出效率改变了。特别是当要处理的数据量大时更为明显。
第二个:
使用排序等有状态操作:
Stream.of("c3","a1","b2","d4","e5")//将数据打乱
.sorted(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
System.out.printf(o1+" ...... "+o2+"\n");
return o1.compareTo(o2);
}
})
.map((s)->{
System.out.println("map: "+s);
return s.toUpperCase();//变为大写
}).filter((s)-> {
System.out.println("filter: "+s);
return s.startsWith("C");//过滤,c开头
}).forEach(s->System.out.println("foreach--"+s));
现在先执行过滤再排序:
Stream.of("c3","a1","b2","d4","e5")//将数据打乱
.filter((s)-> {
System.out.println("filter: "+s);
return s.startsWith("c");//过滤,c开头
}).sorted(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
System.out.printf(o1+" ...... "+o2+"\n");
return o1.compareTo(o2);
}
})
.map((s)->{
System.out.println("map: "+s);
return s.toUpperCase();//变为大写
}).forEach(s->System.out.println("foreach--"+s));
从上面的输出中,我们看到了 sorted从未被调用过,因为经过filter过后的元素已经减少到只有一个,这种情况下,是不用执行排序操作的。因此性能被大大提高了。
2.3、用法
2.3.1、原生流类 :IntStream、LongStream、DoubleStream
IntStream、LongStream的range方法:
IntStream.range(1,5).forEach((s)-> System.out.println(s));
//类似于for(int i=1;i<5;i++){System.out.println(s)}
//LongStream.range(1,5).forEach((s)-> System.out.println(s));
上上图的参数,我们可以知道:原始类型流使用其独有的函数式接口,例如IntFunction代替Function,IntPredicate代替Predicate。
原始类型流支持额外的终端聚合操作,sum()以及average()等
int s = IntStream.range(3,8).sum();
System.out.println(s);
2.3.2、常用的方法
- concat()用于连接两个流
- map方法用于对流中数据进行隐射、
- count()方法执行计数、,min()、max()返回最值操作。
终端操作
- limit(long maxSize) 返回由此流的元素组成的流,截断长度不超过 maxSize 。
- skip(long n) 在丢弃了流 的n个元素后,返回由此流的其余元素组成的流。
- Stream distinct() 返回由此流的不同元素(根据 Object.equals(Object) )组成的流。
- Object[] toArray() 返回包含此流的元素的数组。
- Stream sorted() 返回由此流的元素组成的流,按照自然顺序排序。
Stream sorted(Comparator<? super T> comparator) 返回由此流的元素组成的流,根据提供的 Comparator进行排序。 - boolean noneMatch(Predicate<? super T> predicate) 返回此流的元素是否与提供的谓词匹配。
public class Stream06_CommonMethod {
public static void main(String[] args) {
long length = Stream.of("a", "b", "c", "d").count();//计数
System.out.println(length);
ArrayList<Integer> integerArrayList = new ArrayList<>();
integerArrayList.add(1);
integerArrayList.add(5);
integerArrayList.add(15);
integerArrayList.add(18);
integerArrayList.add(5);
Stream<Integer> stream = integerArrayList.stream();
Optional<Integer> t = stream.max((o1, o2) -> o1 - o2);//求最大值
System.out.println(t + "------" + t.get());
System.out.println("跳过前三个数:");
integerArrayList.stream().skip(3).forEach((s) -> System.out.print(s + " "));//跳过前三个数
System.out.println("\n截取前三个数:");
integerArrayList.stream().limit(3).forEach((s) -> System.out.print(s + " "));//截取前3个数据打印
System.out.println("\n去除重复:");
integerArrayList.stream().distinct().forEach((s) -> System.out.print(s + " "));//去重
System.out.println("\nstream foreach打印所有数:");
integerArrayList.stream().forEach((s) -> System.out.print(s + " "));//打印所有数
System.out.println("\n arrayList foreach打印所有数:");
integerArrayList.forEach((s) -> System.out.print(s + " "));//打印所有数
boolean res = integerArrayList.stream().allMatch((i) -> i > 5);//判断所有的值是否大于5,传入参数为predicate接口。
System.out.println(res);
// Object[] arr = integerArrayList.toArray();
Object[] arr = integerArrayList.stream().toArray();
for (Object o : arr
) {
System.out.println(o);
}
}
}
2.3.3、collect
collect 是一个非常有用的终端操作,它可以将流中的元素转变成另外一个不同的对象,例如一个List,Set或Map。collect 接受入参为Collector(收集器),它由四个不同的操作组成:供应器(supplier)、累加器(accumulator)、组合器(combiner)和终止器(finisher)。
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person("李四",30));
personList.add(new Person("王五",20));
personList.add(new Person("张三",40));
personList.add(new Person("田七",50));
Stream<Person> stream = personList.stream();
//先对集合的person进行过滤掉30以下的人
List<Person> newPersonList = stream.filter((p)->p.getAge()>30).collect(Collectors.toList());
System.out.println(newPersonList);
Map<Integer, List<Person>> personsByAge = personList
.stream()
.collect(Collectors.groupingBy(Person::getAge)); // 以年龄为 key,进行分组
personsByAge.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));
Double averageAge = personList
.stream()
.collect(Collectors.averagingInt(Person::getAge)); // 聚合出平均年龄
System.out.println(averageAge); // 35.0
}
2.3.4、flatMap
上面我们已经学会了如通过map操作, 将流中的对象转换为另一种类型。但是,Map只能将每个对象映射到另一个对象。
如果说,我们想要将一个对象转换为多个其他对象或者根本不做转换操作呢?这个时候,flatMap就派上用场了。
FlatMap 能够将流的每个元素, 转换为其他对象的流。因此,每个对象可以被转换为零个,一个或多个其他对象,并以流的方式返回。之后,这些流的内容会被放入flatMap返回的流中。
package Stream流AndMethodFerence;
public class Stream08_Outer {
Inerr inerr;
}
class Inerr{
Inerr2 inerr2;
}
class Inerr2{
String name="最内侧信息";
}
public class Stream08_FlatMap {
public static void main(String[] args) {
Stream08_Outer stream08_outer = new Stream08_Outer();
//为了处理从 Outer 对象中获取最底层的 name字符串,
//你需要添加多个null检查来避免可能发生的NullPointerException
if(stream08_outer.inerr != null && stream08_outer.inerr.inerr2 != null){
System.out.println(stream08_outer.inerr.inerr2.name);
}
//我们还可以使用Optional的flatMap操作,来完成上述相同功能的判断,且更加优雅
Optional.of(new Stream08_Outer())
.flatMap(o -> Optional.ofNullable(o.inerr))
.flatMap(n -> Optional.ofNullable(n.inerr2))
.flatMap(i -> Optional.ofNullable(i.name))
.ifPresent(System.out::println);
//如果不为空的话,每个flatMap的调用都会返回预期对象的Optional包装,否则返回为null的Optional包装类。
}
}
2.3.5、reduce——将流中的元素规约成流中的一个元素。
规约操作可以将流的所有元素组合成一个结果。Java 8 支持三种不同的reduce方法。
@Override
public final P_OUT reduce(final P_OUT identity, final BinaryOperator<P_OUT> accumulator) {
return evaluate(ReduceOps.makeRef(identity, accumulator, accumulator));
}//接受标识值和BinaryOperator累加器。
@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
return evaluate(ReduceOps.makeRef(accumulator));
}
//BinaryOperator积累函数。该函数实际上是两个操作数类型相同的BiFunction。BiFunction功能和Function一样,但是它接受两个参数。
@Override
public final <R> R reduce(R identity, BiFunction<R, ? super P_OUT, R> accumulator, BinaryOperator<R> combiner) {
return evaluate(ReduceOps.makeRef(identity, accumulator, combiner));
}//三个参数:标识值,BiFunction累加器和类型的组合器函数BinaryOperator。
BinaryOperator是函数式接口,表示对相同类型的两个操作数的操作,并产生结果与操作数类型相同的。这是专业的 {@link BiFunction},表示操作数和结果都是相同的类型的情况
这是一个功能接口,其函数方法为apply(Object, Object)}。
首先构建集合
List<Person> personArrayList = new ArrayList<>();
personArrayList.add(new Person("李四",30));
personArrayList.add(new Person("王五",20));
personArrayList.add(new Person("张三",40));
personArrayList.add(new Person("田七",50));
创建流,使用第一种reduce规约流元素
//第一种情况,reduce只有一个输入参数
personArrayList.stream().reduce((new BinaryOperator<Person>() {
@Override
public Person apply(Person person, Person person2) {
return person.getAge()>person2.getAge()?person:person2;//返回年纪最大的person
}
})).ifPresent((p)-> System.out.println(p));
使用第二种reduce规约:
//第二种情况,reduce有两个输入参数,用于构造一个新的流中元素,
Person newPerson1 = personArrayList.stream().reduce(new Person("二蛋", 100), new BinaryOperator<Person>() {
@Override
public Person apply(Person person, Person person2) {
return person.getAge()>person2.getAge()?person:person2;//返回年纪最大的person,构建的person100大于田七50
}
});
System.out.println(newPerson1.toString());
Person newPerson2 = personArrayList.stream().reduce(new Person("二蛋", 10), new BinaryOperator<Person>() {
@Override
public Person apply(Person person, Person person2) {
return person.getAge()>person2.getAge()?person:person2;//返回年纪最大的person,构建的person100大于田七50
}
});
System.out.println(newPerson2.toString());
可以查看源码:
@Override
public final P_OUT reduce(final P_OUT identity, final BinaryOperator<P_OUT> accumulator) {
return evaluate(ReduceOps.makeRef(identity, accumulator, accumulator));
}
identity直接拿去和之前的流中数据比较。
makeRef(U seed, BiFunction<U, ? super T, U> reducer, BinaryOperator<U> combiner) {
Objects.requireNonNull(reducer);
Objects.requireNonNull(combiner);
class ReducingSink extends Box<U> implements AccumulatingSink<T, U, ReducingSink> {
@Override
public void begin(long size) {
state = seed;
}
@Override
public void accept(T t) {
state = reducer.apply(state, t);
}
@Override
public void combine(ReducingSink other) {
state = combiner.apply(state, other.state);
}
}
return new ReduceOp<T, U, ReducingSink>(StreamShape.REFERENCE) {
@Override
public ReducingSink makeSink() {
return new ReducingSink();
}
};
}
所以我们这里的二蛋年纪不同,最后得到的person也不一样。当然取决于你怎么写BinaryOperator这个接口。
使用第三种reduce约束
首先,我们需要看源码:R是返回值类型。
@Override
public final <R> R reduce(R identity, BiFunction<R, ? super P_OUT, R> accumulator, BinaryOperator<R> combiner) {
return evaluate(ReduceOps.makeRef(identity, accumulator, combiner));
}
测试:
//第三种情况,三个参数,第一个参数指定返回类型,而第二种情况直接指定了person不能改变的。,
// 第二个参数是累加器。其泛型参数的第一个看reduce要返回的类型,第二个则是流中数据的类型。
//第三个参数则是BinaryOperator,其泛型参数都是返回值类型。
Integer ageSum = personArrayList.stream().
reduce(0,(sum,p2)->sum+=p2.getAge(),(sum1,sum2)->sum1+sum2);
System.out.println(ageSum);