概念
流是字节序列的抽象概念。
文件是数据的静态存储形式,而流是指数据传输时的形态。
流类分为两个大类:节点流类和过滤流类(也叫处理流类)。
程序用于直接操作目标设备所对应的类叫节点流类,程序也可以通过一个间接流类去调用节点流类,以达到更加灵活方便地读取各种类型的数据,这个间接流类就是过滤流类(也叫处理流类),或者称为包装类。
包装类的调用过程如下图:
我们先来看看Java里面是怎么定义Stream的:
A sequence of elements supporting sequential and parallel aggregate operations.
我们来解读一下上面的那句话:
- Stream是元素的集合,这点让Stream看起来用些类似Iterator;
- 可以支持顺序和并行的对原Stream进行汇聚的操作;
大家可以把Stream当成一个高级版本的Iterator。原始版本的Iterator,用户只能一个一个的遍历元素并对其执行某些操作;高级版本的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,具体这些操作如何应用到每个元素上,就给Stream就好了!(这个秘籍,一般人我不告诉他:))大家看完这些可能对Stream还没有一个直观的认识,莫急,咱们来段代码。
//Lists是Guava中的一个工具类
List<Integer> nums = Lists.newArrayList(1,null,3,4,null,6);
nums.stream().filter(num -> num != null).count();
上面这段代码是获取一个List中,元素不为null的个数。
流分类的关系
不管流的分类是多么的丰富和复杂,其根源来自于四个基本的类。这个四个类的关系如下:
字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer
Java内用 Unicode 编码存储字符,字符流处理类负责将外部的其他编码的字符流和java内 Unicode 字符流之间的转换。而类InputStreamReader 和OutputStreamWriter处理字符流和字节流的转换。字符流(一次可以处理一个缓冲区)一次操作比字节流(一次一个字节)效率高。
InputStream
由于InputStream和OutputStream是abstact类,所以它们还不能表明具体对应哪种IO设备。它们下面有许多子类,包括网络、管道、内存、文件等具体的IO设备,实际程序中使用的它们的各种子类对象。
注:我们将节点流类所对应的IO源和目标称为流节点(Node)。
注意:将A文件的内容写入B文件,程序对A文件的操作所用的是输出类还是输入类这个问题。输入输出类是相对程序而言的,而不是代表文件的,所以我们应该创建一个输入类来完成对A文件的操作,创建一个输出类来完成对B文件的操作。
OutputStream
以字符为导向的 stream Reader/Writer
以 Unicode 字符为导向的 stream ,表示以 Unicode 字符为单位从 stream 中读取或往 stream 中写入信息。同样,Reader/Writer也为abstact类。
Reader
Writer
IO程序代码的复用:
平时写代码用-1来作为键盘输入的结束,在写的函数中不直接使用System.in,只是在调用该函数时,将System.in作为参数传递进去,这样,我们以后要从某个文件中读取数据,来代替手工键盘输入时,我们可以直接使用这个函数,程序就不用做太多的修改了,达到以不变应万变的效果。
字节流和字符流的相互转换
InputStreamReader和OutputStreamReader:把一个以字节为导向的stream转换成一个以字符为导向的stream。
InputStreamReader类是从字节流到字符流的桥梁:它读入字节,并根据指定的编码方式,将之转换为字符流。
使用的编码方式可能由名称指定,或平台可接受的缺省编码方式。
InputStreamReader的read()方法之一的每次调用,可能促使从基本字节输入流中读取一个或多个字节。
为了达到更高效率,考虑用BufferedReader封装InputStreamReader,
BufferedReaderin = new BufferedReader(new InputStreamReader(System.in));
Stream流的介绍
stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果;
stream不会改变数据源,通常情况下会产生一个新的集合;
stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
对stream操作分为终端操作和中间操作,那么这两者分别代表什么呢?
终端操作:会消费流,这种操作会产生一个结果的,如果一个流被消费过了,那它就不能被重用的。
中间操作:中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。一个特别需要注意的点是:中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。所以中间操作是延迟发生的,中间操作的延迟行为主要是让流API能够更加高效地执行。
stream不可复用,对一个已经进行过终端操作的流再次调用,会抛出异常。
剖析Stream通用语法
图片就是对于Stream例子的一个解析,可以很清楚的看见:原本一条语句被三种颜色的框分割成了三个部分。红色框中的语句是一个Stream的生命开始的地方,负责创建一个Stream实例;绿色框中的语句是赋予Stream灵魂的地方,把一个Stream转换成另外一个Stream,红框的语句生成的是一个包含所有nums变量的Stream,进过绿框的filter方法以后,重新生成了一个过滤掉原nums列表所有null以后的Stream;蓝色框中的语句是丰收的地方,把Stream的里面包含的内容按照某种算法来汇聚成一个值,例子中是获取Stream中包含的元素个数。如果这样解析以后,还不理解,那就只能动用“核武器”–图形化,一图抵千言!
在此我们总结一下使用Stream的基本步骤:
- 创建Stream;
- 转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换);
- 对Stream进行聚合(Reduce)操作,获取想要的结果;
创建流
创建Stream
最常用的创建Stream有两种途径:
- 通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);
- 通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法,后续文章会有介绍)–stream(),把一个Collection对象转换成Stream
使用Stream静态方法来创建Stream
- of方法:有两个overload方法,一个接受变长参数,一个接口单一值
Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
Stream<String> stringStream = Stream.of("taobao");
- generator方法:生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)
Stream.generate(new Supplier<Double>() {
@Override
public Double get() {
return Math.random();
}
});
Stream.generate(() -> Math.random());
Stream.generate(Math::random);
三条语句的作用都是一样的,只是使用了lambda表达式和方法引用的语法来简化代码。每条语句其实都是生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。
- iterate方法:也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万记住使用limit方法,不然会无限打印下去。
通过Collection子类获取Stream
这个在本文的第一个例子中就展示了从List对象获取其对应的Stream对象,如果查看Java doc就可以发现Collection接口有一个stream方法,所以其所有子类都都可以获取对应的Stream对象。
public interface Collection<E> extends Iterable<E> {
//其他方法省略
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
public class Demo1 {
public static void main ( String[] args ) {
//创建stream流,通过Arrays.stream
int[] arr = {1, 2, 3};
IntStream stream = Arrays.stream(arr);
Person[] personStr = {new Person(18, "xiaoliu"), new Person(18, "xiaojing")};
Stream<Person> personStream = Arrays.stream(personStr);
// 通过stream.of
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4);
// 通过集合创建流
List<String> stringList = Arrays.asList("123", "456", "789");
// 创建普通流
Stream<String> stringStream = stringList.stream();
// 创建并行流
Stream<String> parallelStream = stringList.parallelStream();}
}
public class Demo2 {
public static void main ( String[] args ) {
// 流的筛选
List<Integer> integerList = Arrays.asList(1, 2, 3,3, 4, 5, 6);
// 筛选出集合中数字大于4的元素
List<Integer> collect = integerList.stream().filter(x -> x > 4).collect(Collectors.toList());
System.out.println(collect); //[5, 6]
// 集合中的去重
List<Integer> collect1 = integerList.stream().distinct().collect(Collectors.toList());
System.out.println(collect1);//[1, 2, 3, 4, 5, 6]
// 获取流中的第一个元素
Optional<Integer> first = integerList.stream().filter(x -> x > 4).findFirst();
Optional<Integer> any = integerList.stream().filter(x -> x > 4).findAny();
Optional<Integer> any1 = integerList.parallelStream().filter(x -> x > 4).findAny();
System.out.println(first); //Optional[5]
System.out.println(any); //Optional[5]
System.out.println(any1); // 预期结果不稳定
}
}
Stream流中获取最值 max、min和count
public class Demo3 {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("huainvhai", "xiaotiancai", "bennvhai");
// 获取集合中最长的字符串
Optional<String> maxString = stringList.stream().max(Comparator.comparing(String::length));
// 获取集合中最短字符串
Optional<String> minString = stringList.stream().min(Comparator.comparing(String::length));
System.out.println(maxString);
System.out.println(minString);
// 获取集合中的最大值
List<Integer> integerList = Arrays.asList(1, 2, 3);
Optional<Integer> maxInteger = integerList.stream().max((i, j) -> {
return i - j;
});
// 获取集合中的最小值
Optional<Integer> minInteger = integerList.stream().max((i, j) -> {
return j - i;
});
System.out.println(maxInteger);
System.out.println(minInteger);
// 集合泛型是个对象的最值
ArrayList<Person> personList = new ArrayList<>();
personList.add(new Person("xiao",12));
personList.add(new Person("xiao",20));
personList.add(new Person("xiao",18));
Optional<Person> max = personList.stream().max(Comparator.comparing(Person::getAge));
// 获取集合中的元素数量
long count = personList.stream().filter(p -> p.getAge() > 12).count();
System.out.println(max);
System.out.println(count);
}
}
缩减
缩减:就是把一个流缩减成一个值,比如说对一个集合中求和,求乘积等
Stream流定义了三个reduce
public interface Stream<T> extends BaseStream<T, Stream<T>> {
// 方法1
T reduce(T identity, BinaryOperator<T> accumulator);
// 方法2
Optional<T> reduce(BinaryOperator<T> accumulator);
// 方法3
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
}
前两种缩减方式
第一种:接收一个BinaryOperator accumulator function(二元累加计算函数)和identity(标示值)为参数,返回值是一个T类型(代表流中的元素类型)的对象。accumulator代表操作两个值并得到结果的函数。identity按照accumulator函数的规则参与计算,假如函数是求和运算,那么函数的求和结果加上identity就是最终结果,假如函数是求乘积运算,那么函数结果乘以identity就是最终结果。
第二种:不同之处是没有identity,返回值是Optional(JDK8新类,可以存放null)。
public class Demo4 {
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4);
// 写法一 集合中的元素求和 (就是集合中的元素求和再加上1)
Integer integer = integers.stream().reduce(1, Integer::sum);
// 写法二
integers.stream().reduce(1,(x,y)->x+y);
System.out.println(integer); // 11
// 第二种缩减方式 集合中的元素求和
Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
// 写法二
Optional<Integer> reduce1 = integers.stream().reduce((x, y) -> x + y);
System.out.println(reduce); //Optional[10]
System.out.println(reduce1); //Optional[10]
// 集合中使用reduce求最值问题
Optional<Integer> reduce2 = integers.stream().reduce(Integer::max);
System.out.println(reduce2);
/**
* 对象集合求和 求最值问题
*/
ArrayList<Person> personList = new ArrayList<>();
personList.add(new Person("xiao",12));
personList.add(new Person("xiao",20));
personList.add(new Person("xiao",18));
// 求集合中对象的年龄的总和
Optional<Integer> reduce3 = personList.stream().map(p -> p.getAge()).reduce(Integer::sum);
System.out.println(reduce3); //Optional[50]
// 求集合中年龄最大的对象
Optional<Person> reduce4 = personList.stream().reduce((p1, p2) -> p1.getAge() > p2.getAge() ? p1 : p2);
Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getAge));
System.out.println(reduce4);
System.out.println(max);
}
}
collect
collect操作可以接受各种方法作为参数,将流中的元素汇集,
public class Demo5 {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<>();
personList.add(new Person("xiao",12));
personList.add(new Person("xiao",20));
personList.add(new Person("xiao",18));
// 获取平均年龄 averaging
Double collect = personList.stream().collect(Collectors.averagingInt(Person::getAge));
System.out.println(collect); //16.666666666666668
// summarizing
DoubleSummaryStatistics collect1 = personList.stream().collect(Collectors.summarizingDouble(Person::getAge));
System.out.println(collect1); // DoubleSummaryStatistics{count=3, sum=50.000000, min=12.000000, average=16.666667, max=20.000000}
//joining
String collect2 = personList.stream().map(p -> p.getName()).collect(Collectors.joining(","));
System.out.println(collect2); //xiao,xiao,xiao
// reduce
Integer collect3 = personList.stream().collect(Collectors.reducing(0, Person::getAge, (x, y) -> x + y));
Optional<Integer> reduce = personList.stream().map(Person::getAge).reduce(Integer::sum);
System.out.println(collect3); //50
System.out.println(reduce); //Optional[50]
// groupingBy
// 以名字进行分组
Map<String, List<Person>> collect4 = personList.stream().collect(Collectors.groupingBy(Person::getName));
System.out.println(collect4); //{xiao=[Person(name=xiao, age=12), Person(name=xiao, age=20), Person(name=xiao, age=18)]}
// 先以名字分组,再以年龄分组
Map<String, Map<Integer, List<Person>>> collect5 = personList.stream().collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getAge)));
System.out.println(collect5); //{xiao={18=[Person(name=xiao, age=18)], 20=[Person(name=xiao, age=20)], 12=[Person(name=xiao, age=12)]}}
// toList、toSet、toMap
Set<Person> collect6 = personList.stream().collect(Collectors.toSet());
System.out.println(collect6);//[Person(name=xiao, age=18), Person(name=xiao, age=20), Person(name=xiao, age=12)]
}
}
映射
Stream流中,map可以将一个流的元素按照一定的映射规则映射到另一个流中。
public class Demo6 {
public static void main(String[] args) {
String[] strArr = { "abcd", "bcdd", "defde", "ftr" };
Arrays.stream(strArr).map(x->x.toUpperCase()).forEach(System.out::print); //ABCDBCDDDEFDEFTR
List<String> collect = Arrays.stream(strArr).map(x -> x.toUpperCase()).collect(Collectors.toList());
System.out.println(collect); // [ABCD, BCDD, DEFDE, FTR]
}
}
排序
Sorted方法是对流进行排序,并得到一个新的stream流,是一种中间操作。Sorted方法可以使用自然排序或特定比较器。
public class Demo7 {
public static void main(String[] args) {
String[] strArr = { "ab", "bcdd", "defde", "ftr" };
// 自然排序
List<String> collect = Arrays.stream(strArr).sorted().collect(Collectors.toList());
System.out.println(collect); // [ab, bcdd, defde, ftr]
// 自定义排序
// 按照字符串的长度 长度 从小到大
List<String> collect1 = Arrays.stream(strArr).sorted(Comparator.comparing(String::length)).collect(Collectors.toList());
System.out.println(collect1); //[ab, ftr, bcdd, defde]
// 按照字符串的长度逆序排序
List<String> collect2 = Arrays.stream(strArr).sorted(Comparator.comparing(String::length).reversed()).collect(Collectors.toList());
System.out.println(collect2); //[defde, bcdd, ftr, ab]
// 首字母倒序
List<String> collect3 = Arrays.stream(strArr).sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(collect3); //[ftr, defde, bcdd, ab]
// 首字母自然排序
List<String> collect4 = Arrays.stream(strArr).sorted(Comparator.naturalOrder()).collect(Collectors.toList());
System.out.println(collect4); //[ab, bcdd, defde, ftr]
}
}
提取流和组合流
public class Demo8 {
public static void main(String[] args) {
String[] arr1 = {"a","b","c","d"};
String[] arr2 = {"d","e","f","g"};
String[] arr3 = {"i","j","k","l"};
Stream<String> stream1 = Arrays.stream(arr1);
Stream<String> stream2 = Arrays.stream(arr2);
Stream<String> stream3 = Arrays.stream(arr3);
// 可以把两个stream合并成一个stream(合并的stream类型必须相同),只能两两合并
// List<String> collect = Stream.concat(stream1, stream2).collect(Collectors.toList());
// System.out.println(collect); //[a, b, c, d, d, e, f, g]
// 合并去重
List<String> collect1 = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
System.out.println(collect1); //[a, b, c, d, e, f, g]
// limit,限制从流中获得前n个数据
// List<String> collect = collect1.stream().limit(3).collect(Collectors.toList());
// System.out.println(collect); //[a, b, c]
// skip,跳过前n个数据
List<String> collect = collect1.stream().skip(1).limit(3).collect(Collectors.toList());
System.out.println(collect);//[b, c, d]
}
}
汇聚(Reduce)Stream
汇聚这个词,是我自己翻译的,如果大家有更好的翻译,可以在下面留言。在官方文档中是reduce,也叫fold。
在介绍汇聚操作之前,我们先看一下Java doc中对于其定义:
A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count().
简单翻译一下:汇聚操作(也称为折叠)接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。
下面会分两部分来介绍汇聚操作:
- 可变汇聚:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
- 其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;
可变汇聚
可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义(还有其他override方法):
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
先来看看这三个参数的含义:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。看晕了?来段代码!
List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList<Integer>(),
(list, item) -> list.add(item),
(list1, list2) -> list1.addAll(list2));
上面这段代码就是对一个元素是Integer类型的List,先过滤掉全部的null,然后把剩下的元素收集到一个新的List中。进一步看一下collect方法的三个参数,都是lambda形式的函数(上面的代码可以使用方法引用来简化,留给读者自己去思考)。
- 第一个函数生成一个新的ArrayList实例;
- 第二个函数接受两个参数,第一个是前面生成的ArrayList对象,二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
- 第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;
但是上面的collect方法调用也有点太复杂了,没关系!我们来看一下collect方法另外一个override的版本,其依赖Collector。
<R, A> R collect(Collector<? super T, A, R> collector);
这样清爽多了!少年,还有好消息,Java8还给我们提供了Collector的工具类–Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。这样的静态方法还有很多,这里就不一一介绍了,大家可以直接去看JavaDoc。下面看看使用Collectors对于代码的简化:
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());
其他汇聚
– reduce方法:reduce方法非常的通用,后面介绍的count,sum等都可以使用其实现。reduce方法有三个override的方法,本文介绍两个最常用的,最后一个留给读者自己学习。先来看reduce方法的第一种形式,其方法定义如下:
1 |
Optional<T> reduce(BinaryOperator<T> accumulator); |
---|---|
接受一个BinaryOperator类型的参数,在使用的时候我们可以用lambda表达式来。
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -> sum + item).get());
可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。
这个过程可视化的结果如图:
reduce方法还有一个很常用的变种:
1 |
T reduce(T identity, BinaryOperator<T> accumulator); |
---|---|
这个定义上上面已经介绍过的基本一致,不同的是:它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。下面直接给出例子,就不再做说明了。
1 |
List<Integer> ints = Lists.newArrayList(``1``,``2``,``3``,``4``,``5``,``6``,``7``,``8``,``9``,``10``); |
---|---|
2 |
System.out.println(``"ints sum is:" + ints.stream().reduce(``0``, (sum, item) -> sum + item)); |
---|---|
– count方法:获取Stream中元素的个数。比较简单,这里就直接给出例子,不做解释了。
1 |
List<Integer> ints = Lists.newArrayList(``1``,``2``,``3``,``4``,``5``,``6``,``7``,``8``,``9``,``10``); |
---|---|
2 |
System.out.println(``"ints sum is:" + ints.stream().count()); |
---|---|
– 搜索相关
– allMatch:是不是Stream中的所有元素都满足给定的匹配条件
– anyMatch:Stream中是否存在任何一个元素满足匹配条件
– findFirst: 返回Stream中的第一个元素,如果Stream为空,返回空Optional
– noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
– max和min:使用给定的比较器(Operator),返回Stream中的最大|最小值
下面给出allMatch和max的例子,剩下的方法读者当成练习。
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(ints.stream().allMatch(item -> item < 100));
ints.stream().max((o1, o2) -> o1.compareTo(o2)).ifPresent(System.out::println);