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来修改元素的状态,因为这样会破坏函数式编程的原则和可读性。我们应该尽量使用其他终端操作来返回新的结果,而不是修改原来的结果。