‘filter()‘ and ‘map()‘ can be swapped -----JAVA Stream的中间操作

960 阅读9分钟

前言

在工作中,Stream的应用越来越多,有时会看到这样一个警告,其实处理起来也很简单,根据字面意思只需要把filter()和map()替换位置即可,但想要深究其原因,要得仔细了解一下Stream的中间操作


一、Stream都包含哪些操作?

首先Stream的操作中分为中间操作(intermediate operation)和结束操作(terminal operation)

常见的中间操作有

  • filter() 过滤
  • peek() 对每个元素进行操作
  • map() 映射
  • flatMap() 扁平化映射
  • distinct() 去重
  • sorted() 排序
  • skip() 跳过元素数量
  • limit() 指定元素数量

常见的结束操作有

  • forEach
  • toArray
  • reduce
  • collect
  • min
  • max
  • count等

中间操作只是一种标记,只有结束操作才会触发实际计算。

这里我们主要研究的是中间操作

中间操作又可分为有状态中间操作(stateful intermediate operation)和无状态中间操作(intermediate operation)

无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果(先记住这句话,后面去验证)

二、Stream中间操作实现步骤步骤

1.filter、map

代码如下(示例):

@Test
public void testStream01(){
    Stream.of("nice", "to", "meet", "you", "I", "am", "fine")
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.length() == 2;
        })
        .forEach(str -> {
            System.out.println("forEach: " + str);
        });
}
//输出结果
map: 	nice
filter: NICE
map: 	to
filter: TO
forEach: TO
map: 	meet
filter: MEET
map: 	you
filter: YOU
map: 	I
filter: I
map: 	am
filter: AM
forEach: AM
map: 	fine
filter: FINE

从输出结果来看,map、filter、forEach是”垂直“操作,map、filter各执行了7次,如果改变map、filter的操作顺序,将大大减少执行的次数

@Test
public void testStream02(){
    Stream.of("nice", "to", "meet", "you", "I", "am", "fine")
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.length() == 2;
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .forEach(str -> {
            System.out.println("forEach: " + str);
        });
}
//输出结果
filter: nice
filter: to
map: 	to
forEach: TO
filter: meet
filter: you
filter: I
filter: am
map: 	am
forEach: AM
filter: fine

从结果来看,filter还是执行了7次,但是map只执行了2次,只有符合filter的才会执行map操作,这种情况在Stream中有大量元素时,可大幅提高执行效率

这里提示一下,在代码中使用了forEach操作,forEach是个结束操作,如果没有forEach,运行上面两段代码,控制台是不会打印的,可以自己私下试一下,这里不做演示,同时这也解释了上面的一句话:

中间操作只是一种标记,只有结束操作才会触发实际计算

2.sorted

接下来 再看一下另一个中间操作 sorted 代码如下(示例):

@Test
public void testStream03(){
    Stream.of("nice", "to", "meet", "you", "I", "am", "fine")
        .sorted((str1, str2) -> {
            System.out.println("sorted: " + str1 + ", " + str2);
            return str1.compareTo(str2);
        })
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.length() == 2;
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .forEach(str -> {
            System.out.println("forEach: " + str);
        });
}
//输出结果
sorted: to, nice
sorted: meet, to
sorted: meet, to
sorted: meet, nice
sorted: you, nice
sorted: you, to
sorted: I, to
sorted: I, nice
sorted: I, meet
sorted: am, nice
sorted: am, meet
sorted: am, I
sorted: fine, nice
sorted: fine, am
sorted: fine, meet
filter: I
filter: am
map: 	am
forEach: AM
filter: fine
filter: meet
filter: nice
filter: to
map: 	to
forEach: TO
filter: you

从结果中可以看出,sorted操作是”水平“执行的,好像拦腰截断一样,sorted操作处理完之后,filter、map的操作依然时”垂直“的

接下来再次通过调用顺序,尝试优化

@Test
public void testStream04(){
    Stream.of("nice", "to", "meet", "you", "I", "am", "fine")
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.length() == 2;
        })
        .sorted((str1, str2) -> {
            System.out.println("sorted: " + str1 + ", " + str2);
            return str1.compareTo(str2);
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .forEach(str -> {
            System.out.println("forEach: " + str);
        });
}
//输出结果
filter: nice
filter: to
filter: meet
filter: you
filter: I
filter: am
filter: fine
sorted: am, to
map: 	am
forEach: AM
map: 	to
forEach: TO

可以发现,sorted的执行次数明显减少了,其次sorted操作对于filter、map的操作具有截断作用,也就是说sorted前的中间操作,需要完全执行,形成一个完整的Stream流,交给sorted排序。

再来看一个例子

@Test
public void testStream05(){
    Stream.of("nice", "to", "meet", "you", "I", "am", "fine")
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.length() == 3;
        })
        .sorted((str1, str2) -> {
            System.out.println("sorted: " + str1 + ", " + str2);
            return str1.compareTo(str2);
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .forEach(str -> {
            System.out.println("forEach: " + str);
        });
}
//输出结果
filter: nice
filter: to
filter: meet
filter: you
filter: I
filter: am
filter: fine
map: 	you
forEach: YOU

在打印结果中并没有看到有sorted的操作,这是因为经过filter过滤后,流中只剩下一个元素,也就不需要执行排序操作了。

3.小结

通过上面的例子可以看出,通过调整中间操作的调用顺序,在一些情况下可以大幅提高执行效率,另外。所谓的”垂直“操作和”水平“操作其实现过程也是不一样的

来看一眼官方文档 在这里插入图片描述在这里插入图片描述 其实所谓”垂直“操作就是无状态操作,”水平“操作就是有状态操作 结合上面的例子 也就更容易理解上面的这句话了

无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果

再看看一眼官方的解释 在这里插入图片描述 浏览器翻译后: 中间操作进一步分为无状态操作和有状态操作。无状态操作,比如filter和map,在处理新元素时不会保留以前看到的元素的状态——每个元素都可以独立于对其他元素的操作进行处理。当处理新元素时,有状态的操作(如distinct和sorted)可能合并以前看到的元素的状态。

有状态的操作可能需要在产生结果之前处理整个输入。例如,在查看流的所有元素之前,无法通过对流进行排序产生任何结果。因此,在并行计算下,一些包含有状态中间操作的管道可能需要对数据进行多次传递,或者可能需要缓冲重要数据。仅包含无状态中间操作的管道可以单次处理,无论是顺序的还是并行的,数据缓冲最少

这样再看官方的解释是不是更容易理解了 接下来再把其他几个中间操作也都看一下(后面还有惊喜!!!)

4.flatMap、peek、distinct

@Test
public void testStream06(){
    List<Integer> num1 = Arrays.asList(1, 2, 3, 5);
    List<Integer> num2 = Arrays.asList(4, 5, 6);
    List<Integer> num3 = Arrays.asList(7, 8);
    List<List<Integer>> lists = Arrays.asList(num1, num2, num3);
    Stream<Integer> integerStream = lists.stream()
        .flatMap(l -> {
            System.out.println("flatMap: \t" + l);
            return l.stream();
        })
        .filter(num -> {
            System.out.println("filter: " + num);
            return num > 3;
        })
        .peek(num -> {
            System.out.println("peek: " + num);
        });
    System.out.println(integerStream.collect(Collectors.toList()));
}
//输出结果
flatMap: 	[1, 2, 3, 5]
filter: 1
filter: 2
filter: 3
filter: 5
peek: 5
flatMap: 	[4, 5, 6]
filter: 4
peek: 4
filter: 5
peek: 5
filter: 6
peek: 6
flatMap: 	[7, 8]
filter: 7
peek: 7
filter: 8
peek: 8
[5, 4, 5, 6, 7, 8]


@Test
public void testStream07() {
    Stream<String> stringStream = Stream.of("a", "b", "c", "d", "c")
        .distinct()
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.equals("B") || str.equals("C");
        });
    System.out.println(stringStream.collect(Collectors.toList()));
}
//输出结果
map: 	a
filter: A
map: 	b
filter: B
map: 	c
filter: C
map: 	d
filter: D
[B, C]

@Test
public void testStream08() {
    Stream<String> stringStream = Stream.of("a", "b", "c", "d", "c")
        .filter(str -> {
            System.out.println("filter: " + str);
            return str.equals("b") || str.equals("c");
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .distinct();
    System.out.println(stringStream.collect(Collectors.toList()));
}
//输出结果
filter: a
filter: b
map: 	b
filter: c
map: 	c
filter: d
filter: c
map: 	c
[B, C]

testStream06、testStream07、testStream08三个测试可以看出flatMap是无状态操作,distinct是有状态操作,并且调用顺序不同,执行过程也不同,这里不做过多解释,具体根据实际业务需求定夺如果使用 PS:peek作为一个无状态的中间操作,多用在批量更新集合中的元素,另外,peek操作返回一个新的流,也多用在一个流操作中,对后续流操作进行前置操作

5、skip

@Test
public void testStream09() {
    Stream<String> stringStream = Stream.of("c", "a", "c", "b", "d", "e", "c")
        .skip(3)
        .filter(str -> {
            System.out.println("filter: " + str);
            return !str.equals("c");
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        });
    System.out.println(stringStream.collect(Collectors.toList()));
}
//输出结果
filter: b
map: 	b
filter: d
map: 	d
filter: e
map: 	e
filter: c
[B, D, E]

@Test
public void testStream10() {
    Stream<String> stringStream = Stream.of("c", "a", "c", "b", "d", "e", "c")
        .filter(str -> {
            System.out.println("filter: " + str);
            return !str.equals("c");
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .skip(3);
    System.out.println(stringStream.collect(Collectors.toList()));
}
//输出结果
filter: c
filter: a
map: 	a
filter: c
filter: b
map: 	b
filter: d
map: 	d
filter: e
map: 	e
filter: c
[E]

skip操作是跳过指定元素,是有状态操作,没什么好解释,但同样的操作,放在不同的位置执行,结果是不一样的,这一点需要注意

6、limit

@Test
public void testStream11() {
    Stream<String> stringStream = Stream.of("c", "a", "c", "b", "d", "e", "c")
        .filter(str -> {
            System.out.println("filter: " + str);
            return !str.equals("c");
        })
        .map(str -> {
            System.out.println("map: \t" + str);
            return str.toUpperCase();
        })
        .limit(3);
    System.out.println(stringStream.collect(Collectors.toList()));
}

limit和skip都是有状态操作,这没啥好说的,但是在testStream11这个测试代码中,可以先预测一下,Stream中最后两个元素"e", "c"是否会执行filter和map操作? 建议先思考一下 然后再看执行结果 下面是执行结果

filter: c
filter: a
map: 	a
filter: c
filter: b
map: 	b
filter: d
map: 	d
[A, B, D]

可以看到 Stream中最后两个元素"e", "c" 并没有执行filter和map操作,这样看来好像limit又有点像”垂直“操作了?

再看一眼官方文档 在这里插入图片描述 这里说limit是一个可以短路的有状态的中间操作,那么啥是短路操作呢? 继续来看官方的解释,在刚才对有状态中间操作和无状态中间操作的后面还有一句话 在这里插入图片描述 翻译后是: 此外,有些操作被认为是短路操作。如果一个中间操作有无限的输入,结果可能产生有限的流,那么这个中间操作就是短路。当有无限输入时,终端操作可能在有限时间内终止,则为短路。在管道中有一个短路操作是无限流处理在有限时间内正常终止的必要条件,但不是充分条件。

limit是所有中间操作中最特殊的一个

7.'filter()' and 'map()' can be swapped

最最最后,回归正题

@Test
void test() {
    Analysis a1 = new Analysis();
    Analysis a2 = new Analysis();
    Analysis a3 = new Analysis();

    a1.setName("q");
    a1.setValue(new BigDecimal(10));
    a2.setName("q");
    a2.setValue(new BigDecimal(1310));
    a3.setName("q");
    a3.setValue(new BigDecimal(40));

    List<Analysis> pressureRateList = Arrays.asList(a1, a2, a3);

    List<BigDecimal> collect = pressureRateList.stream().map(Analysis::getValue).filter(Objects::nonNull)
        .collect(Collectors.toList());

    List<BigDecimal> collect1 = pressureRateList.stream().filter(a -> a.getValue() != null).map(Analysis::getValue)
        .collect(Collectors.toList());

    System.out.println(collect);
    System.out.println(collect1);

}

这里做了两次stream,一次是map在前filter在后,一次是filter在前map在后,通过上面的学习,貌似先用filter过滤再用map映射好像效率会更高,但是!!! 在这里插入图片描述 在IDEA里,会报 'filter()' and 'map()' can be swapped 这样一个警告,意思是让filter()和map()交换位置

这里的解释是,Analysis的getValue被调用了两次,一次是在过滤器中,另一次是在映射函数中,所以不要这样做两次,最好先映射再过滤


总结

至此,Stream的中间操作都已大致介绍完了,会发现不用的执行顺序,对应的效率是一样的,执行结果也可能不一样,没有必须哪个在前哪个在后,要理解每个中间操作的特性,再结合实际业务需要,才能写出最高效的代码。

参考链接:developer.aliyun.com/article/764…

想要了解更深层的Stream操作过程:blog.csdn.net/qq_40616823…

官方文档:docs.oracle.com/javase/8/do…