Stream流的中间操作和终端操作

最近在写代码时发现一个很有意思的问题

问题代码:

 1 // 1.准备一个集合,排序。
 2         List<Movie> movies = new ArrayList<>();
 3         movies.add(new Movie("摔跤吧,爸爸", 9.5, "阿米尔汗"));
 4         movies.add(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"));
 5         movies.add(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"));
 6         movies.add(new Movie("阿甘正传", 7.5, "汤姆汉克斯"));
 7 // map加工方法(映射):把流上的数据加工成新数据。
 8         System.out.println("-----------------------------------------------");
 9         //第一次map
10         movies.stream().map( movie-> {
11                 movie.setName("电影:"+movie.getName());
12                 return movie;
13             }
14         );
15         //第二次map 加 foreach
16         movies.stream().map( m -> {
17                 m.setName("黑马:" + m.getName());
18                 return m;
19         }).forEach(System.out::println);
20 21 
22         System.out.println("原始数据" +movies);

输出结果:

Movie{name='黑马:摔跤吧,爸爸', score=9.5, actor='阿米尔汗'}
Movie{name='黑马:三傻宝莱坞', score=8.5, actor='阿米尔汗2'}
Movie{name='黑马:三傻宝莱坞', score=8.5, actor='阿米尔汗2'}
Movie{name='黑马:阿甘正传', score=7.5, actor='汤姆汉克斯'}
原始数据[Movie{name='黑马:摔跤吧,爸爸', score=9.5, actor='阿米尔汗'}, Movie{name='黑马:三傻宝莱坞', score=8.5, actor='阿米尔汗2'}, Movie{name='黑马:三傻宝莱坞', score=8.5, actor='阿米尔汗2'}, Movie{name='黑马:阿甘正传', score=7.5, actor='汤姆汉克斯'}]

 

  • 第一个map()方法中没有使用collect()方法来收集加工后的流,而是直接调用了第二个map()方法。这样会导致第一个map()方法的结果被丢弃,可为什么第二个map()方法加上forEach会改变原始数据呢?

为了弄明白其中缘由我查询了一些资料,究其原因和Stream流的中间操作和终端操作有关

在Java 8中,stream是一种抽象的数据结构,它表示一个元素序列,可以对这些元素进行各种操作,比如过滤、映射、排序、聚合等。stream本身并不存储数据,而是从一个源(比如集合、数组、文件等)获取数据,并按照一定的规则处理数据,然后输出到一个目标(比如另一个集合、数组、文件等)。

stream有两种类型的操作:中间操作和终端操作。中间操作是指返回一个新的stream的操作,比如map、filter、sorted等。终端操作是指返回一个非stream的结果的操作,比如forEach、collect、reduce等。

当我们对一个stream进行中间操作时,并不会立即执行这些操作,而是会创建一个新的stream,并记录下这些操作。只有当我们对这个stream进行终端操作时,才会触发这些中间操作的执行,这种机制称为惰性求值

例如,当我们写下以下代码时: List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);  list.stream().map(i -> i * 2); 

并不会立即对list中的每个元素乘以2,而是会返回一个新的stream,并记录下map这个中间操作。只有当我们对这个stream进行终端操作时,比如:

List<Integer> newList = list.stream().map(i -> i * 2).collect(Collectors.toList());

才会触发map这个中间操作的执行,并把结果收集到一个新的列表中。

那么,为什么在后面调用forEach也可以保存修改的对象呢?这是因为forEach是一种特殊的终端操作,它不会返回任何结果,而是对stream中的每个元素执行一个消费者函数(Consumer),这个函数可以对元素进行任何操作,包括修改元素的状态。

例如,当我们写下以下代码时:

List<SampleDTO> list = ...; // 假设list是一个SampleDTO对象的列表
list.stream().forEach(s -> s.setText(s.getText() + "xxx")); // 对每个对象的text属性追加"xxx"

就会触发forEach这个终端操作的执行,并对list中的每个对象执行消费者函数s -> s.setText(s.getText() + “xxx”),这个函数会修改对象的text属性。因此,在执行完这段代码后,list中的每个对象都会被修改。

需要注意的是,虽然forEach可以修改对象的状态,但并不意味着它可以修改stream的源。例如,以下代码是错误的:

List<SampleDTO> list = ...; // 假设list是一个SampleDTO对象的列表
list.stream().forEach(s -> list.remove(s)); // 尝试删除每个对象

这段代码会抛出ConcurrentModificationException异常,因为它试图在遍历list的同时修改list,这是不允许的。

 总之,当我们在后面调用forEach也可以保存修改的对象,是因为forEach是一种特殊的终端操作,它可以对stream中的每个元素执行任何操作,包括修改元素的状态。但是,我们应该避免使用forEach来修改元素的状态,因为这样会破坏函数式编程的原则和可读性。我们应该尽量使用其他终端操作来返回新的结果,而不是修改原来的结果。

热门相关:有个人爱你很久   裙上之臣   修真界败类   裙上之臣   金粉