likes
comments
collection
share

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

作者站长头像
站长
· 阅读数 13

JDK8新特性 --- Lambda表达式 往这跳👉: 从基础到精通,一遍文章读懂 JDK8 Lambda表达式 的使用!

Stream流 与 Collectors工具类

Stream 流

和 Lambda表达式 一样,Stream 也是 JDK8 后出现的新特性,同时也是其中最值得学习的两种新特性之一,当然另一个显然就是 Lambda表达式 了。

这两者在我们实际开发中出现的频率极其频繁。可以毫不夸张说,如果掌握了这两个(Lambda表达式,Stream流),你写代码就跟李白写诗一样,风华绝代、潇洒倜傥,颇有点放荡不羁的感觉~(好像,确实夸张了些...哈哈哈) 总之会非常潇洒就对了!

2.1 此 Stream 非彼 Steam

Steam 想必各位善于劳逸结合的小伙伴都并不陌生吧~ 鄙人偶尔也会浅浅的放松一下。差点扯远了... 确实,此 Stream 非彼 Steam!虽然长得很像,但还是查了个字母 r 的

回归正题,Stream 作为 JDK8 的一大亮点,它是对集合 (Collection) 对象功能的一种增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。

Stream 也叫 Stream流,它是一种流式编程。 如果你想要看官方一点的可以去瞅瞅源码上的注释;而按照我个人的理解,这个流,就可以看作是一个流水线:把所有的产品 (也就是你的数据源) 给弄到传送带上,然后经过一层层的包装 (什么去重呀、过滤呀、组合、排序、统计.....),说白了就是随你怎么弄,弄成你想要的样子,然后出产

2.2 Stream流 初体验

  1. 先整个集合

        @Test
        public void test09(){
            // 数据源
            List<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9);
        }
    

    为了省事,这里用了 Collections.addAll() 的方法,当然你也可以把你想要的元素一直 add 进去

  2. 此时,这个集合就相当于流水线中 待进入流水线的产品,说白了就是数据源 ,那么下面的操作,就是让他进入到流水线中

        @Test
        public void test09(){
            // 传统的方式
            List<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9);
    
            // 进入流水线
            Stream<Integer> stream = list.stream();
        }
    
  3. 可以看到返回的是一个 Stream 对象,也就是我们所说的流对象了,此时就可以在流中去对数据进行操作处理。比如,我们现在需要对这个数据进行去重,把一样的数据都给筛掉,我们可以这样

        @Test
        public void test09(){
            // 传统的方式
            List<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9);
    
            // 进入流水线
            Stream<Integer> stream = list.stream();
    
            // 去重,并打印结果
            stream.distinct().forEach(System.out::println);
        }
    

    这里先看一下流的一个大概操作过程,具体方法后面再细说。

运行后,可以发现重复的字段都给过滤掉了。也就相当于:流水线中我们去把数据源中的一些杂质给过滤出来,从而整成你想要的一个成品返回出来

  1. 上面的是集合作为数据源,或许你会问?那么如果数据源是数组怎么搞?其实一样的,如下:

        @Test
        public void test10(){
            // 数组直接转 stream流
            Integer[] arr = {1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9};
            Stream<Integer> stream1 = Arrays.stream(arr);
    
            // 数组转集合,然后再转 stream
            List<Integer> list = Arrays.asList(arr);
            Stream<Integer> stream2 = list.stream();
        }
    

    两种方式都行,只要能够进入到 "流水线" 那么就可以对其进行操作了

2.3 Stream 中的串行流与并行流

这两个就大概过一下就算了吧,我的理解就是单线程和多线程的区别,当然更官方一点的说法就是:单管道和多管道的区别。

我本想研究一下他们各自低层的实现从而顺带简单的说一下,但后来发现他们的低层走的比较复杂,我也大概的搜了一下,也没有看到我想看的,就没去研究了。

当然,这些都不重要,不是我们当前的所说的重点。后面可能有时间再写一篇这两者之间区别的博客吧... 感兴趣的朋友也可以自己去研究一下。

  • Stream ----- 串行流
  • parallelstream ----- 并行流

整个例子瞅瞅他们运行速率的问题吧:

  1. 传统 for 循环

        @Test
        public void for_test(List<Integer> list){
            long begin = System.currentTimeMillis();
            for (int i = 0; i < list.size(); i++) {
                try {
                    // 睡眠1毫秒  假装在执行操作
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            long end = System.currentTimeMillis();
            System.out.println("for循环所需要用时" + (end - begin));
        }
    
  2. Stream流 遍历

        @Test
        public void stream_test(List<Integer> list){
            long begin = System.currentTimeMillis();
            list.stream().forEach(r -> {
                try {
                    // 睡眠1毫秒  假装在执行操作
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    //            System.out.println(r);
            });
            long end = System.currentTimeMillis();
            System.out.println("stream所需要用时" + (end - begin));
        }
    

    这里打印结果我注释掉了,想看 Stream 与 parallelstream 之间打印的区别的可以瞅瞅:一个有序,一个无序;就跟单线程与多线程一样

  3. parallelstream流 遍历

        @Test
        public void parallelStream_test(List<Integer> list){
            long begin = System.currentTimeMillis();
            list.parallelStream().forEach(r -> {
                try {
                    // 睡眠1毫秒  假装在执行操作
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    //            System.out.println(r);
            });
            long end = System.currentTimeMillis();
            System.out.println("parallelStream所需要用时" + (end - begin));
        }
    
  4. 三个方法都整好后,再整个集合给他们,看他们分别跑了多少毫秒。

        @Test
        public void test11(){
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
            for_test(list);
            stream_test(list);
            parallelStream_test(list);
        }
    

    结果:

    从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

可以看到,在数据量大的时候 parallelStream 快的不是一星半点,速度快了十多倍!那么我们是不是只用 parallelStream 这个就行了,还用 Stream 干啥呢?

其实很多时候都是相对的,效率高随之带来的就是数据不安全的问题了。感兴趣的朋友可以试试打印出 parallelStream_test() 方法的数据,当数据量大时会存在数据丢失的问题...也就是我们所说的线程不安全了

那么怎么解决呢?要么换线程安全的集合,网上看到的建议是用这个 CopyOnWriteArrayList ;要么就是加锁了。

因为这个我鄙人没有去研究过,所以个人感觉吧,既然用了并行流又去加锁,似乎没什么必要,还不如直接用串行流呢...所以对于这两者怎样选择,具体还是要看实际的业务场景的。

还是那句话,这个不是本章的重点。感兴趣的可以研究一下~

2.4 细说 Stream 中的用法

大概了解了 Stream流 的一个过程后,让我们来一起当回厂长,去 "流水线" 中的每一个生产车间去检验检验,瞅瞅这个 "流水线" 都能干些什么?

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

说实话,当这个图弄上来的时候,我都不知道该从哪个先开始...

既然前面 Stream流 初体验 中用到了去重,那么就先从 distinct() 这个先开始吧!

先来搭一些 "前期准备" 吧...

  1. 基础类

    @Data
    @AllArgsConstructor
    public class Book {
    	// 书籍名称
        private String bookName;
    	// 作者
        private String author;
    	// 作者的年龄
        private Integer age;
    	// 书的价格
        private Integer price;
    }
    

    我这里用的是 Lombok 中的 @Data 里面包含了:Getter、Setter、RequiredArgsConstructor、ToString、EqualsAndHashCode

  2. 公用的集合 (内容自己定义吧,不重要... 整几个相同的就行)

        @Test
        public List<Book> getBooks(){
            List<Book> books = new ArrayList<>();
            Collections.addAll(books,
                    new Book("笑傲江湖", "金庸", 99, 365),
                    new Book("笑傲江湖", "金庸", 99, 365),
                    new Book("神雕侠侣", "金庸", 99, 365),
                    new Book("雪中悍刀行","烽火戏诸侯",35,320),
                    new Book("雪中悍刀行","烽火戏诸侯",35,320),
                    new Book("剑来","烽火戏诸侯",35,320),
                    new Book("西游记","吴承恩",56,198),
                    new Book("三国演义","罗贯中",58,230),
                    new Book("水浒传","施耐庵",55,200),
                    new Book("红楼梦","曹雪芹",66,280)
            );
            return books;
        }
    

2.4.1 distinct()

先看源码上的解释:

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

返回不同元素组成的流 (即去重嘛~),规则就是:Object 中的 equals 方法。

所以需要注意的是:当我们实际要去用的时候,是需要重写 hashCode 与 equals 的,它会先比较 hashCode,hashCode 相同时则使用 equals 方法比较 (在 Lombok 就一个 @Data 啥都不用管了)

  1. 怎么用呢?代码如下:

        @Test
        public void test13(){
            List<Book> books = getBooks();
            books.stream()
                    .distinct()
                    .forEach(System.out::println);
        }
    

    从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

  2. 注意!!一定要重写 hashCode 和 equals ,不然就会失效了。如果使用 Lombok 的小伙伴可以试试把 Book 类中的 @Data 给注释了,加上个 @ToString 去看看没有重写的结果就知道了

    //@Data
    @ToString
    @AllArgsConstructor
    public class Book {
    
        private String bookName;
    
        private String author;
    
        private Integer age;
    
        private Integer price;
    }
    

    再跑一遍,就会看到这个集合原来怎么样,现在也还是怎么样 ( distinct() 失效了)。

    从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

2.4.2 sorted()

这个就是排序。在 Stream 中 sorted() 有两个重载方法,一个无参的,默认按自然排序 (即正序排序);另一个需要实现 Comparator 这个函数式接口,自己定义排序规则

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

那么我们就整个场景:按作者的年龄来排序

  1. 首先来瞅瞅无参的这个,先浅看一波源码

    从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

    可以看到 this.isNaturalSort = true 是否是自然排序设置了为 true,即下面那里调用个的这个方法: Comparator.naturalOrder()

    这个不扯那么远,大概的意思就是无参的这个排序 Stream 中默认使用了自然排序 (也就是正序了)

  2. 那既然他默认帮咱们做了排序了,那我们直接调来瞅瞅是啥样子的效果?

        @Test
        public void test14(){
            List<Book> books = getBooks();
            books.stream()
                    .sorted()
                    .forEach(System.out::println);
        }
    

    运行!映入眼前的是一片红...

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

不要慌,源码上有一句注释:Will throw CCE when we try to sort if T is not Comparable ,即如果T不可比较,当我们尝试排序时将抛出CCE (也就是当前的ClassCastException)。

确实咱把一个对象 Book 都扔过去了,谁知道你要比较啥?所以我们可以这样做:让 Book 这个类实现的 Comparable 接口 ,通过接口提供的 compareTo 方法来进行排序。

  1. 所以这个 Book 对象可以这样:

    @Data
    @AllArgsConstructor
    public class Book implements Comparable<Book>{
    
        private String bookName;
    
        private String author;
    
        private Integer age;
    
        private Integer price;
    
        @Override
        public int compareTo(@NotNull Book o) {
            return age - o.age;
        }
    }
    
  2. 再次运行,你会发现运行成功,且效果也与预期一样

    从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

    或许有些好兄弟已经发现了!咱们这样做,说到底还是通过实现了 Comparable接口 从而自定义了排序的规则,似乎和 sorted() 没啥关系。说白了,就是一顿 "脱裤子放屁" 的 XX 操作...

    确实,当前的场景下用这个无参的去排序本身就是不太合理的,它感觉更适合在某些较为单一的数据中去使用。所以在不一样的场景下,使用不一样的方法是很有必要的

  3. 在 Stream 中还给我们提供了另一个方法,那么此时我们就可以这样做了:

        @Test
        public void test14(){
            List<Book> books = getBooks();
            books.stream()
                    .sorted(Comparator.comparingInt(Book::getAge))
                    .forEach(System.out::println);
        }
    

    即在 sorted(Comparator<? super T> comparator) 这个方法中里面是要一个 Comparator 的实现,而这个又是一个函数式接口,所以可以在里面使用 Lambda表达式,具体可以看我上一篇文章

    运行的结果也是一样的,就不粘出来了

2.4.3 filter()

条件过滤,根据具体的条件去过滤数据,而不满足的则会被剔除掉。

Stream<T> filter(Predicate<? super T> predicate);

直接看一个 Demo 吧:最近资金紧缺,过滤掉大于300元的书,买不起

    @Test
    public void test15(){
        List<Book> books = getBooks();
        books.stream()
                .filter(book -> book.getPrice() <= 300)
                .forEach(System.out::println);
    }

很简单的一个方法,想要实现怎样的效果,只需在方法中去定义自己的放回就可以了

2.4.4 forEach()

这个就不用多说啦~老熟客了,这个方法要传入一个 Consumer 函数接口对象,没有返回值

void forEach(Consumer<? super T> action);

当然,这个需要说明一下的是,该方法的在执行操作的时候,行为是不确定的,即在并行操作里面,该方法不能保证按顺序执行

感兴趣的朋友可以自己写个 Demo 用并行流去试一下,当数据量大的时候应该可以看出变化

2.4.5 forEachOrdered()

这个其实与 forEach() 是一样的,只是不同的是,该方法保证在串行流或者是并行流中都能够保证元素按顺序执行

void forEachOrdered(Consumer<? super T> action);

2.4.6 count()

这个也是在 Stream 流中非常简单的一个,它的作用:用于统计流中的元素数量,返回的 long 类型

long count();

2.4.7 max() & min()

这两个就跟孪生兄弟一样,相差不大。只是一个 获取流中最大的元素 ,一个则 获取流中最小的元素

Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);

这两个方法都需要传入一个 Comparator 函数式接口对象,该接口中有一个compare方法,用于自定义大小比较规则。

那么要怎样去用呢?这个与前面的 sorted 那里的也是一样的,大同小异。

  1. 找出哪本书价格最高

        @Test
        public void test16(){
            List<Book> books = getBooks();
            Book book = books.stream()
                    .max(Comparator.comparing(Book::getPrice))
                    .get();
            System.out.println("book = " + book);
        }
    
  2. 找出哪本书价格最低也是一样的,max 变成了 min 而已

        @Test
        public void test16(){
            List<Book> books = getBooks();
            Book book = books.stream()
                    .min(Comparator.comparing(Book::getPrice))
                    .get();
            System.out.println("book = " + book);
        }
    
  3. 或者说你有一串数字,要找出最大值或者最小值

        @Test
        public void test17() {
            Integer num = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .max(Integer::compareTo)
                    .get();
            System.out.println("num = " + num);		// num = 18
        }
    

    或许你会有疑问,Integer::compareTo 这是啥?

    在 Integer 源码中可以看到 Integer 是实现了 Comparable<> 接口的,当然也重写了其 compareTo() 方法

        public int compareTo(Integer anotherInteger) {
            return compare(this.value, anotherInteger.value);
        }
    
        public static int compare(int x, int y) {
            return (x < y) ? -1 : ((x == y) ? 0 : 1);
        }
    

2.4.8 reduce()

reduce() 是一种聚合操作,即将多个值经过特定的计算后获取到的单个值。像上面已经介绍了的 count()max()min() 等都是聚合操作。

就像刚说的,你会发现其都是将流中多个值经过特定的计算后得到的单个值

reduce() 有三个重载方法

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

Optional reduce(BinaryOperator accumulator);

这个与其他两个相比,应该是最为常用的一个了。和上面几个聚合函数一样,该函数的返回值也是 Optional 对象,因为结果存在空指针的情况。

  1. 求集合中元素的和

        @Test
        public void test18(){
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .reduce((x, y) -> x + y)
                    .get();
            System.out.println("i = " + i);		// i = 65
        }
    

    即大概是这个意思,当然下面的相乘也是这个道理。最终的目的都还是将多个值经过特定的计算后获取到的单个值

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

  1. 求集合中元素的积

        @Test
        public void test18(){
            int i = Stream.of(1, 2, 3, 4, 5)
                    .reduce((x, y) -> x * y)
                    .get();
            System.out.println("i = " + i);		// i = 120
        }
    

    如果说想要更清晰的知道它是怎样执行的,也可以 DEBUG 去瞅瞅,或者像这个打印一下出来

        @Test
        public void test18(){
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .reduce((x, y) -> {
                        System.out.println("x = " + x + "   " + "y =" + y);
                        return x + y;
                    }).get();
            System.out.println("i = " + i);		// i = 65
        }
    

T reduce(T identity, BinaryOperator accumulator);

T identity 类似于是一个默认值,即当集合为空时,就返回这个默认值;当然如果集合不为空时,这个值也会参与到计算当中

  1. 还是上面那个例子,就集合中元素的和

        @Test
        public void test19(){
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .reduce(0, (x, y) -> x + y);
            System.out.println("i = " + i);     // i = 65
        }
    

    但是,但集合为空时,那么 T identity 这个就是充当默认值的角色了

        @Test
        public void test19(){
            List<Integer> list = new ArrayList<>();
            int i = list
                	.stream()
                    .reduce(0, (x, y) -> x + y);
            System.out.println("i = " + i);     // i = 0
        }
    

    那么集合不为空时,这个值也会参与到计算当中又当如何理解呢?试一下就知道了

        @Test
        public void test19(){
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .reduce(5, (x, y) -> x + y);
            System.out.println("i = " + i);     // i = 70
    
            int j = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .reduce(3, (x, y) -> x + y);
            System.out.println("j = " + j);     // j = 68
        }
    

U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator combiner);

这个就稍微复杂很多了。

在方法中我们可以看到有两个泛型:TU ,其中 T 是集合中的元素类型;而 U 是计算之后返回结果的类型,即 U 的类型是由第一个参数 identity 决定的

即该方法可以返回与集合中元素不同类型的值,而前两个方法则只能返回与集合中元素相同的值

像这样:

    @Test
    public void test21(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70)
        ).collect(Collectors.toList());

        List<Integer> reduce = books.stream().reduce(new ArrayList<Integer>(), new BiFunction<ArrayList<Integer>, Book, ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> integers, Book book) {
                integers.add(book.getPrice());
                System.out.println("list = " + integers);
                System.out.println("bookPrice = " + book.getPrice());
                return integers;
            }
        }, new BinaryOperator<ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> integers1, ArrayList<Integer> integers2) {
                integers1.addAll(integers2);
                System.out.println("integers1 = " + integers1);
                System.out.println("integers2 = " + integers2);
                return integers1;
            }
        });

        System.out.println("finalList = " + reduce);
    }

运行之后的输出结果:

list = [100]
bookPrice = 100
list = [100, 60]
bookPrice = 60
list = [100, 60, 70]
bookPrice = 70
finalList = [100, 60, 70]

可以看到,经过一顿操作之后类型发生了转变,这个是没问题的;但有没有发现,似乎 BinaryOperator 中的方法没有打印出来?那让我们再看一个:

    @Test
    public void test20(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70)
        ).collect(Collectors.toList());

        Integer i = books.parallelStream().reduce(0, new BiFunction<Integer, Book, Integer>() {
            @Override
            public Integer apply(Integer integer, Book book) {
                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer = " + integer);
                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "bookPrice = " + book.getPrice());
                return integer + book.getPrice();
            }
        }, new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer integer1, Integer integer2) {
                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer1 = " + integer1);
                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer2 = " + integer2);
                return integer1 + integer2;
            }
        });

        System.out.println("i = " + i);
    }

stream() 换成了 parallelStream() ,即从串行流换成并行流,并且在打印前面加上当前线程的 ID ,运行后如下:

线程 1 ===> integer = 0
线程 17 ===> integer = 0
线程 16 ===> integer = 0
线程 16 ===> bookPrice = 100
线程 17 ===> bookPrice = 70
线程 1 ===> bookPrice = 60
线程 1 ===> integer1 = 60
线程 1 ===> integer2 = 70
线程 1 ===> integer1 = 100
线程 1 ===> integer2 = 130
i = 230

那么为什么 BinaryOperator 没有执行呢?这是因为 Stream 是支持并发操作的,为了避免竞争,对于 reduce 这个方法,线程都会有独立的 result,BinaryOperator combiner 的作用在于汇总所有线程的计算结果,从而得到一个最终的 result

上述可以多运行几次或者多加一些数据量去观察规律,进程在多线程中都会争夺时间片,因此每次运行的结果不一定相同

2.4.9 limit() & skip()

  • limit :限制,截取流中指定数量的元素
  • skip :跳过,跳过流中指定数量的元素
  1. 获取最便宜的3本书

        @Test
        public void test22(){
            List<Book> books = getBooks();
            books.stream()
                    .sorted(Comparator.comparing(Book::getPrice))   // 排序
                    .limit(3)                                       // 截取
                    .forEach(System.out::println);                  // 打印输出
        }
    
  2. 跳过也是一样的,跳过最贵的5本书

        @Test
        public void test22(){
            List<Book> books = getBooks();
            books.stream()
                    .sorted(Comparator.comparing(Book::getPrice,Comparator.reverseOrder()))     // 排序
                    .skip(5)                                                                    // 截取
                    .forEach(System.out::println);                                              // 打印输出
        }
    

这两个比较简单,直接对流中的元素使用即可

2.4.10 allMatch() & anyMatch() & noneMatch()

这三个算是一组的,返回的都是布尔类型,用于对一些逻辑的判断。

boolean allMatch(Predicate<? super T> predicate);

boolean anyMatch(Predicate<? super T> predicate);

boolean noneMatch(Predicate<? super T> predicate);
  • allMatch() 当流中的元素都按指定的规则匹配上,才会返回 true (即全部对才对)
  • anyMatch() 当流中有任意元素满足指定的规则时,返回 true (即对一个就对了)
  • noneMatch() 当流中所有元素都没有与指定的规则匹配上,才会返回 true (即全部错了才是对的)
  1. 是否所有的书都大于60元

        @Test
        public void test23(){
            List<Book> books = Stream.of(
                    new Book("剑来", "烽火", 38, 100),
                    new Book("斗破", "土豆", 34, 60),
                    new Book("完美", "辰东", 37, 70),
                    new Book("斗罗", "三少", 36, 80)
            ).collect(Collectors.toList());
    
            boolean b = books.stream().allMatch(book -> book.getPrice() > 60);
            System.out.println("是否所有的书都大于60元 = " + b);      // false
        }
    

    allMatch() 所有都满足,返回 true,否之则 false

  2. 是否有高于60元的书

        @Test
        public void test23(){
            List<Book> books = Stream.of(
                    new Book("剑来", "烽火", 38, 100),
                    new Book("斗破", "土豆", 34, 60),
                    new Book("完美", "辰东", 37, 70),
                    new Book("斗罗", "三少", 36, 80)
            ).collect(Collectors.toList());
    
            boolean b = books.stream().anyMatch(book -> book.getPrice() > 60);
            System.out.println("是否有高于60元的书 = " + b);      // true
        }
    

    anyMatch() 有一个满足,返回 true,否之则 false

  3. 第三个的例子其实有点难举,因为个人感觉有点变扭。

        @Test
        public void test23(){
            List<Book> books = Stream.of(
                    new Book("剑来", "烽火", 38, 100),
                    new Book("斗破", "土豆", 34, 60),
                    new Book("完美", "辰东", 37, 70),
                    new Book("斗罗", "三少", 36, 80)
            ).collect(Collectors.toList());
    
            boolean b = books.stream().noneMatch(book -> book.getPrice() > 110);
            System.out.println("b = " + b);      // true
        }
    

    这个有点像是与 allMatch() 反着来,只有当流中的所有元素,都不满足指定的规则时,才会返回 true,否之则 false

    这个多调整几次数据,运行几遍看看效果便能琢磨出规律。

2.4.11 map()

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

这个方法提供了一个映射规则,对流中的数据进行映射,从而用新的数据替换旧的数据

这个方法的使用频率很高,而且也非常的实用

  1. 获取书的名称

        @Test
        public void test25(){
            List<Book> books = Stream.of(
                    new Book("剑来", "烽火", 38, 100),
                    new Book("斗破", "土豆", 34, 60),
                    new Book("完美", "辰东", 37, 70),
                    new Book("斗罗", "三少", 36, 80)
            ).collect(Collectors.toList());
    
            books.stream()
                    .map(Book::getBookName)
                    .forEach(System.out::println);
        }
    
  2. 当然你也可以这样,不过这里需要把你想要的数据 return 出去

        @Test
        public void test25(){
            List<Book> books = Stream.of(
                    new Book("剑来", "烽火", 38, 100),
                    new Book("斗破", "土豆", 34, 60),
                    new Book("完美", "辰东", 37, 70),
                    new Book("斗罗", "三少", 36, 80)
            ).collect(Collectors.toList());
    
            books.stream()
                    .map(book -> {
                        String bookName = book.getBookName();
                        String author = book.getAuthor();
                        return bookName + "---" + author;
                    })
                    .forEach(System.out::println);
        }
    

2.4.12 flatMap()

扁平化映射,其实本质上与 map() 是一样的,都是对流中的元素进行一定的处理然后再返回出来

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

那么什么时候使用 flatMap() 什么时候又使用 map() 呢?

网上大佬一句话总结:多层数据结构转单层时用 flatmap(),单层转单层或者多层转多层用 map()

  1. 那么我们试一下在集合里面放数组是个怎样的效果

        @Test
        public void test26() {
            List<String[]> collect = Stream.of(
                    new String[]{"a,b,c"},
                    new String[]{"d,e,f"},
                    new String[]{"g,h,j"}
            ).collect(Collectors.toList());
    
            collect.stream()
                    .flatMap(new Function<String[], Stream<?>>() {
                        @Override
                        public Stream<?> apply(String[] strings) {
                            // 可以看到,流中的元素是数组。那么我们可以将数组装换成 Stream 类型来处理
                            return Arrays.stream(strings);
                        }
                    })
                    .forEach(System.out::println);
        }
    

    我们以往处理的时候,在集合中只有一种数据结构;而此时集合里面放的是数组,那么此时这个 扁平化 这个操作就用的上了

    先对集合中的数据结构作处理,然后再进行数据处理

  2. 也还可以这样玩一下:把里面的字母变成大写

        @Test
        public void test27() {
            List<String[]> collect = Stream.of(
                    new String[]{"a,b,c"},
                    new String[]{"d,e,f"},
                    new String[]{"g,h,j"}
            ).collect(Collectors.toList());
    
            collect.stream()
                    .flatMap(Arrays::stream)
                    .map(String::toUpperCase)
                    .forEach(System.out::println);
        }
    

    这里用的是 Lambda 表达式的形式

2.4.13 mapToInt() & mapToDouble() & mapToLong()

IntStream mapToInt(ToIntFunction<? super T> mapper);

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

LongStream mapToLong(ToLongFunction<? super T> mapper);

这几个方法比较好玩,将流中的元素转换成了 IntStream/DoubleStream/LongStream 从而可以进行一些运算,比如:获取数据最大值、最小值、数据量、平均值等

  1. 获取数据最大值

        @Test
        public void test28() {
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .mapToInt(value -> value)
                    .max()
                    .getAsInt();
            System.out.println("i = " + i);		// 18
        }
    
  2. 获取数据的和

        @Test
        public void test28() {
            int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .mapToInt(value -> value)
                    .sum();
            System.out.println("i = " + i);		// 65
        }
    
  3. 获取数据的平均值

        @Test
        public void test28() {
            double i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .mapToInt(value -> value)
                    .average()
                    .getAsDouble();
            System.out.println("i = " + i);		// 6.5
        }
    
  4. 也可以对这组数据进行分析

        @Test
        public void test28() {
            IntSummaryStatistics statistics = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                    .mapToInt(value -> value)
                    .summaryStatistics();
            System.out.println("数据分析 = " + statistics);		// 数据分析 : IntSummaryStatistics{count=10, sum=65, min=1, average=6.500000, max=18}
        }
    

这个一组与之对应的还有一些 扁平化 的操作:

IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);

DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);

本质上是与上面是一样的,什么时候用哪个,围绕这句话即可: 多层数据结构转单层时用 flat 的,单层转单层或者多层转多层用普通的

2.4.14 collect()

这个可能是用的最多的几个方法之一了吧

    <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
                  
	<R, A> R collect(Collector<? super T, A, R> collector);

collect() 多参的方法,我似乎没见人用过。不过在源码上写了一两个例子可以瞅瞅

        // 将字符串累积到ArrayList中   stringStream 假设该流是存在的
        List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

        // 将获取一个字符串流并将它们连接成一个字符串   stringStream 假设该流是存在的
        String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();

用的最多的还是这个 collect() 单参的方法 ,那么就不得不说一下 Collectors

Collectors 是一个工具类,里面封装了很多方法,以便于我们对流中的数据进行处理和整合

所以咱们直接来瞅瞅 Collectors 工具类的使用吧

2.5 Collectors 工具类

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

通过这个图片可以看到,Collectors工具类 给我们提供了一堆的方法;这里的方法甚至于比 Stream流 里面更多

因为我们再使用 Stream流 时往往都脱离不了最后的 collect() 这一步,把经过 “流水线” 后的元素给收集起来返回出去。所以,掌握 Collectors工具类 的使用就十分的关键了

2.5.1 toList() & toSet()

    public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }
    
    public static <T>
    Collector<T, ?, Set<T>> toSet() {
        return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_UNORDERED_ID);
    }

将流中的元素收集到一个新的集合当中,不过这并不能保证返回的集合的类型、可变型、可序列化性或线程安全性等。不过你也可以定制一下,使用 toCollection() 这个方法自己去定制

这两个方法用的是相对比较多的,特别是 toList() 。往往我们处理完数据后都是收集成一个 List 返回出去

    @Test
    public void test31(){
        List<Integer> collectList = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10).collect(Collectors.toList());
        System.out.println("collectList = " + collectList);

        Set<Integer> collectSet = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10).collect(Collectors.toSet());
        System.out.println("collectSet = " + collectSet);
    }

即一个返回一个 ArrayList 的集合,一个则返回 HashSet 的集合

2.5.2 toCollection()

    public static <T, C extends Collection<T>>
    Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                                   (r1, r2) -> { r1.addAll(r2); return r1; },
                                   CH_ID);
    }

如果与上面的 toList() 或者 toSet() 比较的话,你会发现这里 CollectorImpl 中的第一个参数传的是 collectionFactory ,而上面那里传的则是 ArrayList::newHashSet::new

那此时我们就明了了,实际上不就是实例化一个对象吗!那么咱们自己去定制的话便可以这样写了

    @Test
    public void test32(){
        TreeSet<Integer> collectTree = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10).collect(Collectors.toCollection(TreeSet::new));
        
        ConcurrentHashSet<Integer> collectConcurrentSet = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10).collect(Collectors.toCollection(ConcurrentHashSet::new));
    }

用匿名内部类的形式可能会更清晰一些:

    @Test
    public void test33(){
        TreeSet<Integer> collectTree = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10)
                .collect(Collectors.toCollection(new Supplier<TreeSet<Integer>>() {
                    @Override
                    public TreeSet<Integer> get() {
                        return new TreeSet<>();
                    }
                }));
        System.out.println("collectTree = " + collectTree);
    }

那个 ConcurrentHashSet 也是一样的,这里就不写了

2.5.3 toMap()

Stream流 转换为 Map对象 ,该方法有三个重载

  1. 第一个重载
    public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
    }

可以看到这个方法有两个入参,keyMappervalueMapper ,其实就是让你指定你要流里面的哪些元素作为 key ;哪些元素作为 value

    @Test
    public void test34(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Map<String, Integer> map = books.stream().collect(Collectors.toMap(Book::getBookName, Book::getPrice));
        map.forEach((k,v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

输出打印后的结果:

key : 斗破  |  value : 60
key : 完美  |  value : 70
key : 斗罗  |  value : 80
key : 剑来  |  value : 100

需要注意的是,该方法是不允许有 key 重复的,如果出现 key 重复的情况,方法会直接抛出异常;


  1. 所以针对这种情况,方法的 第二个重载 给出了解决的方法
    public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
        return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
    }

这个除了指定 key 与 value 之外,还多了个参数 mergeFunction 这个是合并函数,用于解决与同一键关联的值之间的冲突问题

这个函数的意思就是说,如果当出现 key 重复的情况时,是使用前者作为 key 亦或是使用后者作为 key 再亦或是别的另外些什么值

    @Test
    public void test35(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("剑来", "烽火戏诸侯", 38, 100),
                new Book("斗破", "天蚕土豆", 34, 60),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());
        
        Map<String, String> map = books.stream().collect(Collectors.toMap(Book::getBookName, Book::getAuthor, (k1, k2) -> k2));
        map.forEach((k,v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

输出结果:

key : 斗破  |  value : 土豆
key : 完美  |  value : 辰东
key : 斗罗  |  value : 三少
key : 剑来  |  value : 烽火戏诸侯

通过对比,可以看出:一个重复的 key = “剑来”,前者的作者应该是:烽火 的;而后者是:烽火戏诸侯;而通过 mergeFunction 这个函数:(k1, k2) -> k2 当 key 相同时,选择了后者 k2 的 value。这种便是前者与后者,选择了后者的这种情况了

当然如果 (k1, k2) -> k1 那么便是 前者与后者,选择了前者的这种情况了

细心观察一下区别让后多运行几次,很快便能看出区别

当然,对该函数 mergeFunction 的操作还不仅于此,你可以自己去定义规则,就像上面说的可以使用一些别的由你自己去定制的值去作 key。例如:k1 + k2 的 value 之和作为一个该 key 的新 value

    @Test
    public void test35(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("剑来", "烽火戏诸侯", 38, 100),
                new Book("斗破", "天蚕土豆", 34, 60),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Map<String, String> map = books.stream().collect(Collectors.toMap(Book::getBookName, Book::getAuthor, (k1, k2) -> k1 + k2));
        map.forEach((k,v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

输出结果:

key : 斗破  |  value : 天蚕土豆土豆
key : 完美  |  value : 辰东
key : 斗罗  |  value : 三少
key : 剑来  |  value : 烽火烽火戏诸侯

可以看到 (k1, k2) -> k1 + k2 当 k1 的 v1 = 天蚕土豆 加上了 k2 的 v2 = 土豆 从而输出了 : 天蚕土豆土豆

这种则是前者与后者都不选择,用自己指定的方式来操作


  1. 这是第三个重载
    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier) {
        BiConsumer<M, T> accumulator
                = (map, element) -> map.merge(keyMapper.apply(element),
                                              valueMapper.apply(element), mergeFunction);
        return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
    }

通过源码可以看到,这第三个重载除了该函数 mergeFunction 之外 ,还有一个这样的东西:Supplier<M> mapSupplier

哎~或许你会说,怎么感觉这么熟悉,似乎在哪里见过?没错,这确实是刚刚 toCollection() 方法中的一个入参。

各位小伙伴 可以回头去瞅瞅,前两个重载的源码中,你会发现都有一个 HashMap::new 的操作。因为这是默认的返回类型 HashMap 。那么,也就是说,这个参数的作用是 当我们不想返回默认的类型 HashMap 时,我们可以通过这个 Supplier mapSupplier 参数,从而返回我们自己想要的类型

用法与前面的 toCollection() 是一样的,这里就大概的过一下

    @Test
    public void test36(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
            	new Book("斗破", "天蚕土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());
        
        Map<String, Integer> map = books.stream().collect(Collectors.toMap(Book::getBookName, Book::getPrice, (k1,k2) -> k2, TreeMap::new));
        map.forEach((k,v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

通过这样的方式,最终我们返回来 TreeMap::new 的多态实例,自定义了返回的类型

2.5.4 toConcurrentMap()

这个其实与 toMap() 方法是一样的,只是一个是线程安全带 ,一个是线程不安全的。其三个重载方法都是一模一样的,所以这里就不多介绍了

    public static <T, K, U>
    Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                                        Function<? super T, ? extends U> valueMapper) {
        return toConcurrentMap(keyMapper, valueMapper, throwingMerger(), ConcurrentHashMap::new);
    }
    
    public static <T, K, U>
    Collector<T, ?, ConcurrentMap<K,U>>
    toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                    Function<? super T, ? extends U> valueMapper,
                    BinaryOperator<U> mergeFunction) {
        return toConcurrentMap(keyMapper, valueMapper, mergeFunction, ConcurrentHashMap::new);
    }
    
        public static <T, K, U, M extends ConcurrentMap<K, U>>
    Collector<T, ?, M> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                       Function<? super T, ? extends U> valueMapper,
                                       BinaryOperator<U> mergeFunction,
                                       Supplier<M> mapSupplier) {
        BiConsumer<M, T> accumulator
                = (map, element) -> map.merge(keyMapper.apply(element),
                                              valueMapper.apply(element), mergeFunction);
        return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_CONCURRENT_ID);
    }

可以看到,与 toMap() 不一样的地方就是:

  • toMap() : HashMap::new;线程不安全的
  • toConcurrentMap() : ConcurrentHashMap::new;线程安全的

2.5.5 joining()

该方法返回的是一个 收集器 ,这个 收集器会将流中的元素按照特定的符号连接成一个字符串返回

这个方法也是有三个重载

  1. 第一个重载方法
public static Collector<CharSequence, ?, String> joining() {
    return new CollectorImpl<CharSequence, StringBuilder, String>(
            StringBuilder::new, StringBuilder::append,
            (r1, r2) -> { r1.append(r2); return r1; },
            StringBuilder::toString, CH_NOID);
}

从源码就可以看到,new 了一个 StringBuilder 的实力,然后一顿操作,把流中的元素一个个 append 上去

    @Test
    public void test38(){
        List<String> joinBefore = Stream.of("tian", "zen", "me", "bu", "hui", "ta").collect(Collectors.toList());
        System.out.println("joinBefore = " + joinBefore);

        String joinAfter = joinBefore.stream().collect(Collectors.joining());
        System.out.println("joinAfter = " + joinAfter);
    }

打印结果:

joinBefore = [tian, zen, me, bu, hui, ta]
joinAfter = tianzenmebuhuita

即默认就是直接拼接


  1. 第二个重载
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
        return joining(delimiter, "", "");
    }

这个重载方法是有入参的,该入参就是在元素与元素之间所需分割的符号。所以该方法会 按匹配顺序连接输入元素,由指定的分隔符分隔,然后返回出去

    @Test
    public void test38(){
        List<String> joinBefore = Stream.of("tian", "zen", "me", "bu", "hui", "ta").collect(Collectors.toList());
        System.out.println("joinBefore = " + joinBefore);

        String joinAfter = joinBefore.stream().collect(Collectors.joining("-"));
        System.out.println("joinAfter = " + joinAfter);
    }

输出结果:

joinBefore = [tian, zen, me, bu, hui, ta]
joinAfter = tian-zen-me-bu-hui-ta

  1. 第三个重载
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }

这个其实就是多了个 prefix 和 suffix ,即在前面和后面加上一些你指定的符号。

其实在 第二个重载方法 里面,都是调用该方法,只是 prefix 和 suffix 传了个空字符串而已 return joining(delimiter, "", "")

    @Test
    public void test38(){
        List<String> joinBefore = Stream.of("tian", "zen", "me", "bu", "hui", "ta").collect(Collectors.toList());
        System.out.println("joinBefore = " + joinBefore);

        String joinAfter = joinBefore.stream().collect(Collectors.joining("-","<",">"));
        System.out.println("joinAfter = " + joinAfter);
    }

输出结果:

joinBefore = [tian, zen, me, bu, hui, ta]
joinAfter = <tian-zen-me-bu-hui-ta>

2.5.6 groupingBy()

该方法返回的是一个 收集器 ,这个 收集器会将流中的元素进行分组操作,根据特定的分类来对元素进行分组,并在 Map 中返回

先来看一下源码

    public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return groupingBy(classifier, toList());
    }

classifier 是一个分类器,即将输入元素映射到键的分类器函数

通过源码可以看到,该方法调用了两个参数的 groupingBy() 方法,同时这里把 classifier 这个分类器也传过去了,而且还多传了个 toList() 方法。哎?是否觉得很眼熟,这里面返回的 List 默认就是调用了 toList() 方法,那么也就是说是ArrayList 的实例 (ArrayList::new)

那么我们继续去瞅一眼两个参数的 groupingBy() 方法都干了啥

    public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }

这两个参数的 groupingBy() 方法又去调了三个参数的 groupingBy() 方法,这个先不管 。

继续上面的,可以看到该两个参数的方法中的入参除了 分类器 classifier 之外还有一个 Collector<? super T, A, D> downstream ,那么看到这里,上面传了个 toList() 方法也就恍然大悟了。

那么也就是说,在这个两个参数的 groupingBy() 中可以继续调用 Collectors工具类 来处理元素

那么这里去调用了三个参数的 groupingBy() 方法,其中两个传参都搞明白了,那么还有一个 HashMap::new .....

哎?如果从前面一直看下来的朋友,是否又觉得这个很眼熟呢?这里两个参数的 groupingBy() 方法默认返回的 Map 是 HashMap 类型的,那么不会又可以自己去定义返回的集合吧?

你别说,好像还真的是呢!那么我们来看三个参数的 groupingBy()

    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
		// 源码省略,感兴趣的可以自己去瞅瞅
    }

参数 Supplier<M> mapFactory ,其实这个在前面的 toCollection()以及 toMap() 那里便出现了 ,其用应该也是差不多的

所以说,在 groupingBy() 这个方法中,你也可以根据自己的需求去定制返回出去的集合


来瞅瞅这几个例子

新建一个对象

@Data
@AllArgsConstructor
public class World {

    private String country;

    private String province;

}

先来看第一个种,单个入参的

    @Test
    public void test39() {
        List<World> worlds = Stream.of(
                new World("中国", "香港"),
                new World("中国", "澳门"),
                new World("中国", "台湾"),
                new World("俄罗斯", "莫斯科"),
                new World("俄罗斯", "圣披德堡"),
                new World("泰国", "曼谷"),
                new World("英国", "伦敦")
        ).collect(Collectors.toList());

        Map<String, List<World>> map = worlds.stream()
                .collect(Collectors.groupingBy(World::getCountry));
        map.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

运行结果:

key : 泰国  |  value : [World(country=泰国, province=曼谷)]
key : 俄罗斯  |  value : [World(country=俄罗斯, province=莫斯科), World(country=俄罗斯, province=圣披德堡)]
key : 中国  |  value : [World(country=中国, province=香港), World(country=中国, province=澳门), World(country=中国, province=台湾)]
key : 英国  |  value : [World(country=英国, province=伦敦)]

就像刚开始说的,默认返回的是 ArrayList 的实例,所以在这里可以看到泛型里面是一个 List (Map<String, List>)


那么两个参数的方法,则是对 List<> 返回的一个处理了

比如这样,像上面返回的一样,我不想 value 中是一个 world 对象,我觉得太冗余了,我只想要对象中的国家,即最终是 key = 国家;value = 省份 。那么此时就可以这样去处理一下了

    @Test
    public void test40() {
        List<World> worlds = Stream.of(
                new World("中国", "香港"),
                new World("中国", "澳门"),
                new World("中国", "台湾"),
                new World("俄罗斯", "莫斯科"),
                new World("俄罗斯", "圣披德堡"),
                new World("泰国", "曼谷"),
                new World("英国", "伦敦")
        ).collect(Collectors.toList());

        Map<String, List<String>> map = worlds.stream()
                .collect(Collectors.groupingBy(World::getCountry,
                        Collectors.mapping(World::getProvince, Collectors.toList()))
                );
        map.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

运行结果:

key : 泰国  |  value : [曼谷]
key : 俄罗斯  |  value : [莫斯科, 圣披德堡]
key : 中国  |  value : [香港, 澳门, 台湾]
key : 英国  |  value : [伦敦]

至于三个参数的方法,那么就跟前面说的差不多了

    @Test
    public void test41() {
        List<World> worlds = Stream.of(
                new World("中国", "香港"),
                new World("中国", "澳门"),
                new World("中国", "台湾"),
                new World("俄罗斯", "莫斯科"),
                new World("俄罗斯", "圣披德堡"),
                new World("泰国", "曼谷"),
                new World("英国", "伦敦")
        ).collect(Collectors.toList());

        TreeMap<String, List<String>> map = worlds.stream()
                .collect(Collectors.groupingBy(World::getCountry,
                        TreeMap::new,
                        Collectors.mapping(World::getProvince, Collectors.toList()))
                );
        map.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

输出结果:

key : 中国  |  value : [香港, 澳门, 台湾]
key : 俄罗斯  |  value : [莫斯科, 圣披德堡]
key : 泰国  |  value : [曼谷]
key : 英国  |  value : [伦敦]

可以看到 TreeMap::new 传进去之后,返回的就是一个 TreeMap<String, List>

2.5.7 groupingByConcurrent()

这个就如同 toMap() 与 toConcurrentMap() 一般,一个是线程不安全的,一个则是线程安全的;这个也是一样的,groupingBy() 是线程不安全的,groupingByConcurrent() 则是线程安全的

    public static <T, K>
    Collector<T, ?, ConcurrentMap<K, List<T>>>
    groupingByConcurrent(Function<? super T, ? extends K> classifier) {
        return groupingByConcurrent(classifier, ConcurrentHashMap::new, toList());
    }
    public static <T, K, A, D>
    Collector<T, ?, ConcurrentMap<K, D>> groupingByConcurrent(Function<? super T, ? extends K> classifier,
                                                              Collector<? super T, A, D> downstream) {
        return groupingByConcurrent(classifier, ConcurrentHashMap::new, downstream);
    }
    public static <T, K, A, D, M extends ConcurrentMap<K, D>>
    Collector<T, ?, M> groupingByConcurrent(Function<? super T, ? extends K> classifier,
                                            Supplier<M> mapFactory,
                                            Collector<? super T, A, D> downstream) {
		// 源码省略,感兴趣的可以自己去瞅瞅
    }

通过这几个重载方法,可以发现 ,在默认的情况下,返回的 Map 的类型由原来的 HashMap::new 变成了 ConcurrentHashMap::new 。HashMap 是线程不安全的,而 ConcurrentHashMap 则是线程安全的,所以其实两者的区别就十分明了了

至于用法,其实与 groupingBy() 差不多,所以这里就不再举例了

2.5.8 summarizingInt() & summarizingDouble() & summarizingLong()

这三个是一样的,只是对应的数据类型不一样;但具体的方法以及实现都是差不多的

咱直接先瞅瞅源码

    public static <T>
    Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) {
        return new CollectorImpl<T, IntSummaryStatistics, IntSummaryStatistics>(
                IntSummaryStatistics::new,
                (r, t) -> r.accept(mapper.applyAsInt(t)),
                (l, r) -> { l.combine(r); return l; }, CH_ID);
    }
    public static <T>
    Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) {
        return new CollectorImpl<T, DoubleSummaryStatistics, DoubleSummaryStatistics>(
                DoubleSummaryStatistics::new,
                (r, t) -> r.accept(mapper.applyAsDouble(t)),
                (l, r) -> { l.combine(r); return l; }, CH_ID);
    }
    public static <T>
    Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper) {
        return new CollectorImpl<T, LongSummaryStatistics, LongSummaryStatistics>(
                LongSummaryStatistics::new,
                (r, t) -> r.accept(mapper.applyAsLong(t)),
                (l, r) -> { l.combine(r); return l; }, CH_ID);
    }

这几个方法中都分别有一个这样的实现:IntSummaryStatistics::newDoubleSummaryStatistics::newLongSummaryStatistics::new

是否觉得有些眼熟?没错,这个也在前面出现过:

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

是不是又清晰明了了?没错,该方法返回的是对数据的一个分析整合,包括 count、sum、min、max、avg

    @Test
    public void test43(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        IntSummaryStatistics collect = books.stream().collect(Collectors.summarizingInt(Book::getPrice));
        System.out.println("collect = " + collect);
        
        // 当然你也可以单个去获取 IntSummaryStatistics 对象中的值
        System.out.println("collect.getCount() = " + collect.getCount());
        System.out.println("collect.getSum() = " + collect.getSum());
        System.out.println("collect.getAverage() = " + collect.getAverage());
        System.out.println("collect.getMax() = " + collect.getMax());
        System.out.println("collect.getMin() = " + collect.getMin());
    }

输出的结果:

collect = IntSummaryStatistics{count=4, sum=310, min=60, average=77.500000, max=100}
collect.getCount() = 4
collect.getSum() = 310
collect.getAverage() = 77.5
collect.getMax() = 100
collect.getMin() = 60

或许有人会问:前面 Stream 流中已经提供了一个这样的方法了,为什么 Collectors工具类 中又提供了一个,这不是多此一举吗?

其实这个一般来说是结合分组去使用的,比如现在有这样一个例子:商店有好几本书,其中因出版社以及质量不同从而导致书的价格也不一样,那么可以这样去看:

    @Test
    public void test44(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("剑来", "烽火", 38, 120),
                new Book("剑来", "烽火", 38, 160),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("完美", "辰东", 37, 90),
                new Book("斗罗", "三少", 36, 50)
        ).collect(Collectors.toList());

        Map<String, IntSummaryStatistics> collect = books.stream().collect(Collectors.groupingBy(Book::getBookName, Collectors.summarizingInt(Book::getPrice)));
        collect.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

当然这个例子可能不太合适,但大概是这个意思。输出的结果:

key : 斗破  |  value : IntSummaryStatistics{count=1, sum=60, min=60, average=60.000000, max=60}
key : 完美  |  value : IntSummaryStatistics{count=2, sum=160, min=70, average=80.000000, max=90}
key : 斗罗  |  value : IntSummaryStatistics{count=1, sum=50, min=50, average=50.000000, max=50}
key : 剑来  |  value : IntSummaryStatistics{count=3, sum=380, min=100, average=126.666667, max=160}

至于其他两个:summarizingDouble()summarizingLong() 用法一样的,只是数据类似不一样,具体可根据实际去选择

2.5.9 summingInt() & summingDouble() & summingLong()

这三个与前面的三个极其相似,但其实还是有些区别的。

  • summarizing:总结,即数据分析统计
  • summing:求和

这一组比较简单,源码就不瞅了,直接瞅个例子:

    @Test
    public void test45(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Integer sum1 = books.stream().collect(Collectors.summingInt(Book::getPrice));
        System.out.println("sum1 = " + sum1);       // sum1 = 310

        Integer sum2 = books.stream().mapToInt(Book::getPrice).sum();
        System.out.println("sum2 = " + sum2);       // sum2 = 310
    }

就直接调用即可,返回的是一个求和后的结果。

其实该方法就等同于上面 IntSummaryStatistics.getSum() 也等同于 mapToInt().getSum()

其他两个方法 summingDouble()summingLong() 同义

2.5.10 averagingInt() & averagingDouble() & averagingLong()

这一组也是一样的,就是 获取平均数

也是很简单,跟上面几乎一模一样:

    @Test
    public void test46(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 100),
                new Book("剑来", "烽火", 38, 130),
                new Book("斗破", "土豆", 34, 60),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Double avg = books.stream().collect(Collectors.averagingInt(Book::getPrice));
        System.out.println("avg = " + avg);     // avg = 88.0
    }

**该方法就等同于上面 IntSummaryStatistics.getAverage() 也等同于 mapToInt().average().getAsDouble() **

2.5.11 reducing()

public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)

public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op)
    
public static <T, U> Collector<T, ?, U> reducing(U identity,
                                Function<? super T, ? extends U> mapper,
                                BinaryOperator<U> op)

reducing() 方法其实与 Stream 中的 reduce() 方法差不多的;单个入参以及两个入参的方法基本一致,至于三个入参的方法,从入参上来看就稍微有一些些不一样了,这个后面再聊

就像上面说的,与 reduce() 方法类似,所以这个也是一个聚合函数,用来执行一些二目运算,求和,聚集等操作

    @Test
    public void test51(){
        Integer num1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).collect(Collectors.reducing((i1, i2) -> i1 + i2)).get();
        Integer num2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce((i1, i2) -> i1 + i2).get();
        System.out.println("num1 = " + num1);	// num1 = 45
        System.out.println("num2 = " + num2);	// num2 = 45
    }

而两个入参的方法也是,第一个参数:T identity 其实也是类似一个初始值的东西

    @Test
    public void test51(){
        Integer num1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).collect(Collectors.reducing(0, (i1, i2) -> i1 + i2));
        Integer num2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (i1, i2) -> i1 + i2);
        System.out.println("num1 = " + num1);	// num1 = 45
        System.out.println("num2 = " + num2);   // num2 = 45

        Integer num3 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).collect(Collectors.reducing(5, (i1, i2) -> i1 + i2));
        Integer num4 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(5, (i1, i2) -> i1 + i2);
        System.out.println("num3 = " + num3);	// num3 = 50
        System.out.println("num4 = " + num4);   // num4 = 50
    }

仔细留意一下测试代码,与第一个入参的方法相比,少了一个 get() 方法,因为两个入参的方法的返回并是不 Optional 的类型,因为少了此时的 T identity 就类似一个默认值,并不会有空指针的情况出现,再怎么不济也有一个默认值在。所以当集合为空时,返回的就是这里的默认值了,如下:

    @Test
    public void test52(){
        List<Integer> list = new ArrayList<>();
        Integer num1 = list.stream().collect(Collectors.reducing(5, (i1, i2) -> i1 + i2));
        Integer num2 = list.stream().reduce(5, (i1, i2) -> i1 + i2);
        System.out.println("num1 = " + num1);   // num1 = 5
        System.out.println("num2 = " + num2);   // num2 = 5
    }

可以看到,尽管我在 reducing() 这个方法中进行了一些操作,但实际上是没有意义的,因为我只 new 了一个集合,但是集合里面是没有 add 值的。也就是说,这是一个空集合,由此看出,所以在这里有默认值的存在,返回的就不是一个 Optional 类型


至于第三个入参的,就稍微有些麻烦了。

从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

  1. reducing() 中的第二个参数 Function<? super T, ? extends U> 在这里是做了一个转换,没有做一个聚合处理;

    而在 reduce() 中的第二个入参 BiFunction<U, ? super T, U> 则是聚合处理后的结果了

  2. reducing() 中的第三个参数 BinaryOperator<U> op,则是进行的一个聚合操作;

    reduce() 中的第三个参数 BinaryOperator<U> combiner ,这个是在并行流中才会有具体的效果

    @Test
    public void test53() {
        Integer collect = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .collect(Collectors.reducing(0,
                        new Function<Integer, Integer>() {
                            @Override
                            public Integer apply(Integer integer) {
                                System.out.println("第二个入参的值 = " + integer);
                                return integer;
                            }
                        }, new BinaryOperator<Integer>() {
                            @Override
                            public Integer apply(Integer integer1, Integer integer2) {
                                System.out.println("integer1 = " + integer1 + "   " + "integer2 = " + integer2);
                                return integer1 + integer2;
                            }
                        }));

        System.out.println("collect = " + collect);
    }

运行结果:

第二个入参的值 = 1
integer1 = 0   integer2 = 1
第二个入参的值 = 2
integer1 = 1   integer2 = 2
第二个入参的值 = 3
integer1 = 3   integer2 = 3
第二个入参的值 = 4
integer1 = 6   integer2 = 4
第二个入参的值 = 5
integer1 = 10   integer2 = 5
第二个入参的值 = 6
integer1 = 15   integer2 = 6
第二个入参的值 = 7
integer1 = 21   integer2 = 7
第二个入参的值 = 8
integer1 = 28   integer2 = 8
第二个入参的值 = 9
integer1 = 36   integer2 = 9
collect = 45

用匿名函数的方式可能会清晰很多,所以这种多参的就不用 Lambda表达式 的写法了。

通过打印,可以比较的清晰的知道该方法的执行过程,所以此时在第二个参数的时候做一些操作,便能达到一个 转换 的效果了;而第三个参数,也正如前面所说,做的是一个聚合的操作

那么下面再回头瞅瞅 Stream 里面的 reduce()

    @Test
    public void test54(){
        Integer reduce = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .reduce(0,
                        new BiFunction<Integer, Integer, Integer>() {
                            @Override
                            public Integer apply(Integer integer1, Integer integer2) {
                                System.out.println("int1 = " + integer1 + "   " + "int2 = " + integer2);
                                return integer1 + integer2;
                            }
                        }, new BinaryOperator<Integer>() {
                            @Override
                            public Integer apply(Integer integer1, Integer integer2) {
                                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer1 = " + integer1 + "   " + "integer2 = " + integer2);
                                return integer1 + integer2;
                            }
                        });

        System.out.println("reduce = " + reduce);
    }

运行结果:

int1 = 0   int2 = 1
int1 = 1   int2 = 2
int1 = 3   int2 = 3
int1 = 6   int2 = 4
int1 = 10   int2 = 5
int1 = 15   int2 = 6
int1 = 21   int2 = 7
int1 = 28   int2 = 8
int1 = 36   int2 = 9
reduce = 45

reduce() 方法也正如我们上面所说的,在第二个入参中是执行的聚合操作,也就是相当于 reducing() 方法里面的第三个入参做的事情

那么此时 reduce() 方法的第三个入参,我们可以发现:啥也没干,根本没有打印出相关的结果。这是因为 BinaryOperator<U> combiner ,这个是在并行流中才会有具体的效果

    @Test
    public void test54(){
        Integer reduce = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .parallel()     // 注意这里!这里是转成并行流的
                .reduce(0,
                        new BiFunction<Integer, Integer, Integer>() {
                            @Override
                            public Integer apply(Integer integer1, Integer integer2) {
                                System.out.println("int1 = " + integer1 + "   " + "int2 = " + integer2);
                                return integer1 + integer2;
                            }
                        }, new BinaryOperator<Integer>() {
                            @Override
                            public Integer apply(Integer integer1, Integer integer2) {
                                System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer1 = " + integer1 + "   " + "integer2 = " + integer2);
                                return integer1 + integer2;
                            }
                        });

        System.out.println("reduce = " + reduce);
    }

打印结果:

int1 = 0   int2 = 6
int1 = 0   int2 = 5
线程 1 ===> integer1 = 5   integer2 = 6
int1 = 0   int2 = 7
int1 = 0   int2 = 9
int1 = 0   int2 = 4
int1 = 0   int2 = 3
线程 16 ===> integer1 = 3   integer2 = 4
int1 = 0   int2 = 2
int1 = 0   int2 = 8
线程 1 ===> integer1 = 8   integer2 = 9
线程 1 ===> integer1 = 7   integer2 = 17
线程 1 ===> integer1 = 11   integer2 = 24
int1 = 0   int2 = 1
线程 19 ===> integer1 = 1   integer2 = 2
线程 19 ===> integer1 = 3   integer2 = 7
线程 19 ===> integer1 = 10   integer2 = 35
reduce = 45

此时,第三个入参中打印的内容便可观察到了。

与其他的方法相比,该方法用的会相对少很多,更多的还是对一些数据进行聚合,当然,感兴趣的朋友可以多次运行观察一下其中的规律,进行更深层次的深究加深对该方法的理解

2.5.12 maxBy() & minBy()

其实在 reducing() 这个方法之前,本想先介绍一下这两个方法的,后来点进源码一看,才发现这两个方法的其实就是基于 reducing() 这个方法实现的:

    public static <T> Collector<T, ?, Optional<T>>
    maxBy(Comparator<? super T> comparator) {
        return reducing(BinaryOperator.maxBy(comparator));
    }
    public static <T> Collector<T, ?, Optional<T>>
    minBy(Comparator<? super T> comparator) {
        return reducing(BinaryOperator.minBy(comparator));
    }

BinaryOperator.minBy() 可以看到,里面是传了这个方法;其实这一类都是一种聚合操作,即将多个值经过特定的计算后获取到的单个值

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

通过源码可以看到,这里作了一个比较,(a, b) -> comparator.compare(a, b) >= 0 ? a : b

这两个方法与 Stream流中的 max() & min() 相差无几的

    @Test
    public void test55(){
        List<Integer> collect = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).collect(Collectors.toList());

        Integer max1 = collect.stream().collect(Collectors.maxBy((x,y) -> x - y)).get();
        System.out.println("max1 = " + max1);       // max1 = 9
        Integer max2 = collect.stream().max((x, y) -> x - y).get();
        System.out.println("max2 = " + max2);       // max2 = 9

        Integer min1 = collect.stream().collect(Collectors.minBy((x,y) -> x - y)).get();
        System.out.println("min1 = " + min1);       // min1 = 1
        Integer min2 = collect.stream().min((x, y) -> x - y).get();
        System.out.println("min2 = " + min2);       // min2 = 1
    }

其实也很好理解,就拿 maxBy() 来说,假设传进去是 1 和 2,那么就是 1 - 2 >= 0 ? 1 : 2 明显这里返回的是 2,所以当流走到最后的时候,由此便得出了最大值了

那么如果是:maxBy((x,y) -> y - x) 换了个位置,那就是 2 - 1 >= 0 ? 1 : 2 这里就返回了1了,所以此时就返回了最小值

当然了,你也可以交给已经存在了的方法,不自己去写

    @Test
    public void test56(){
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .collect(Collectors.maxBy(Comparator.comparingInt(value -> value)))
                .ifPresent(System.out::println);	// 9
    }

2.5.13 partitioningBy()

分组,这个有点类似于 groupingBy() ,不过 partitioningBy() 会比较单一一些,它会根据 Predicate<? super T> predicate 从而使数据分成 true与false 两组

    public static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return partitioningBy(predicate, toList());
    }
    public static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream) {
        // 省略...
    }

从单个入参 Predicate<? super T> predicate 中可以看到,其低层其实是调了自己两个入参的方法,即 Map 里面中的 value 默认是一个 ArrayList ,因为我们知道,Collectors工具类中的 toList() 方法低层是有一步:ArrayList::new 的操作

来瞅一个例子:

    @Test
    public void test61(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 130),
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("斗破", "土豆", 34, 80),
                new Book("斗破", "土豆", 34, 100),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Map<Boolean, List<Book>> collect = books.stream().collect(Collectors.partitioningBy(book -> book.getPrice() >= 80));

        collect.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

输出结果:

key : false  |  value : [Book(bookName=斗破, author=土豆, age=34, price=60), Book(bookName=完美, author=辰东, age=37, price=70)]

key : true  |  value : [Book(bookName=剑来, author=烽火, age=38, price=130), Book(bookName=剑来, author=烽火, age=38, price=100), Book(bookName=斗破, author=土豆, age=34, price=80), Book(bookName=斗破, author=土豆, age=34, price=100), Book(bookName=斗罗, author=三少, age=36, price=80)]

那么这里干了什么呢?主要是这一段:Collectors.partitioningBy(book -> book.getPrice() >= 80) 这里需要传一个最终返回为 boolean 的值,即我们这里根据:书的价格是否大于等于80元 去分类,从而得出两组数据

我们前面说的,单个入参的 partitioningBy() 方法默认是调了 toList() ,所以这里返回的也是一个 List

那么 partitioningBy() 方法的第二个入参 Collector<? super T, A, D> downstream 则是一个组合器。源码中有对应的解释:将下游还原的收集器,其实它主要的作用就是用来返回一个自定义的下游

    @Test
    public void test61(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 130),
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("斗破", "土豆", 34, 80),
                new Book("斗破", "土豆", 34, 100),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        Map<Boolean, Set<Book>> setMap = books.stream()
                .collect(Collectors.partitioningBy(book -> book.getPrice() >= 80,
                        Collectors.toSet()));

        Map<Boolean, HashSet<Book>> hashSetMap = books.stream()
                .collect(Collectors.partitioningBy(book -> book.getPrice() >= 80,
                        Collectors.toCollection(HashSet::new)));

        Map<Boolean, Map<String, Integer>> mapMap = books.stream()
                .collect(Collectors.partitioningBy(book -> book.getPrice() >= 80,
                        Collectors.toMap(Book::getBookName, Book::getPrice,(k1, k2) -> k2)));

        setMap.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));

        hashSetMap.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));

        mapMap.forEach((k, v) -> System.out.println("key : " + k + "  |  " + "value : " + v));
    }

以此类推,可以根据具体的需要返回自定义的下游。输出结果:

key : false  |  value : [Book(bookName=完美, author=辰东, age=37, price=70), Book(bookName=斗破, author=土豆, age=34, price=60)]
key : true  |  value : [Book(bookName=斗破, author=土豆, age=34, price=100), Book(bookName=剑来, author=烽火, age=38, price=100), Book(bookName=剑来, author=烽火, age=38, price=130), Book(bookName=斗破, author=土豆, age=34, price=80), Book(bookName=斗罗, author=三少, age=36, price=80)]

key : false  |  value : [Book(bookName=完美, author=辰东, age=37, price=70), Book(bookName=斗破, author=土豆, age=34, price=60)]
key : true  |  value : [Book(bookName=斗破, author=土豆, age=34, price=100), Book(bookName=剑来, author=烽火, age=38, price=100), Book(bookName=剑来, author=烽火, age=38, price=130), Book(bookName=斗破, author=土豆, age=34, price=80), Book(bookName=斗罗, author=三少, age=36, price=80)]

key : false  |  value : {斗破=60, 完美=70}
key : true  |  value : {斗破=100, 斗罗=80, 剑来=100}

2.5.14 collectingAndThen()

这个方法就如它的命名一样,收集之后再进行一些操作。其实这个方法还挺好玩的,也确实省事很多,可以减少一些代码量,使代码变得更为的 优雅 ,不过似乎能用上此方法的场景确实不多。

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream,
                                                                Function<R,RR> finisher) {
    // 省略...
}

Collector<T,A,R> downstream 这个已经是老熟客了,看到它就基本能够知道,大概是用来处理一些返回的数据类型的了;

Function<R,RR> finisher 至于这个,则是对上流返回的结果,即第一个入参所返回的结果再此进行处理

因为模拟一些比较复杂的使用场景实在是太麻烦了,所以这里只展示一些用法,打开一种数据处理的思路,并不考虑是不是“脱裤子放屁”的行为(手动捂脸)

    @Test
    public void test59(){
        List<Book> books = Stream.of(
                new Book("剑来", "烽火", 38, 130),
                new Book("剑来", "烽火", 38, 100),
                new Book("斗破", "土豆", 34, 60),
                new Book("斗破", "土豆", 34, 80),
                new Book("斗破", "土豆", 34, 100),
                new Book("完美", "辰东", 37, 70),
                new Book("斗罗", "三少", 36, 80)
        ).collect(Collectors.toList());

        List<Book> collect = books.stream()
                .collect(Collectors.collectingAndThen(
                        Collectors.partitioningBy(book -> book.getPrice() >= 80, Collectors.toList()),  // 根据价格是否大于等于80分组
                        bookMap -> bookMap
                                .entrySet()
                                .stream()
                                .filter(Map.Entry::getKey)  // 过滤价格大于等于80的书籍
                                .map(Map.Entry::getValue)
                                .flatMap(Collection::stream)
                                .collect(Collectors.toList())
                ));
        collect.forEach(System.out::println);
    }

输出结果:

Book(bookName=剑来, author=烽火, age=38, price=130)
Book(bookName=剑来, author=烽火, age=38, price=100)
Book(bookName=斗破, author=土豆, age=34, price=80)
Book(bookName=斗破, author=土豆, age=34, price=100)
Book(bookName=斗罗, author=三少, age=36, price=80)

当然,上面一大堆,其实两行代码也能搞定了:

        books.removeIf(book -> book.getPrice() < 80);
        books.forEach(System.out::println);

效果也是一样的,不过我们重点瞅瞅该方法的用法就好了

为了更清晰一些,咱们再瞅一个:

    @Test
    public void test62(){
        List<World> worlds = Stream.of(
                new World("中国", "香港"),
                new World("中国", "澳门"),
                new World("中国", "台湾"),
                new World("俄罗斯", "莫斯科"),
                new World("俄罗斯", "圣披德堡"),
                new World("泰国", "曼谷"),
                new World("英国", "伦敦")
        ).collect(Collectors.toList());

        String collect = worlds.stream()
                .collect(Collectors.collectingAndThen(
                        Collectors.groupingBy(World::getCountry, Collectors.mapping(World::getProvince, Collectors.toList())),      // key = country,value = 地区的集合 的一个 map
                        worldMap -> worldMap    // 对该 map 继续进行处理
                                .values()       // 拿到 map 的 values,下面就是一些常规的 stream流 中的处理操作了
                                .stream()
                                .flatMap(Collection::stream)
                                .collect(Collectors.joining("--"))
                ));
        System.out.println("collect = " + collect);     // collect = 曼谷--莫斯科--圣披德堡--香港--澳门--台湾--伦敦
    }

其实就是等同于下面这样,只不过是将两个步骤合在了同一个方法里面去写了

        Map<String, List<String>> map = worlds.stream()
                .collect(Collectors.groupingBy(World::getCountry, Collectors.mapping(World::getProvince, Collectors.toList())));

        String collect = map.values()
                .stream()
                .flatMap(Collection::stream)
                .collect(Collectors.joining("--"));

        System.out.println("collect = " + collect);     // collect = 曼谷--莫斯科--圣披德堡--香港--澳门--台湾--伦敦

在对数据处理的时候,这个方法其实还是挺强的,能为我们省很多事。当然,关于该方法的一些用法,网上也还有很多案例,感兴趣的可以去深造一下

2.6 总结

JDK8 中带来的新特性是真的非常强大!而作为 Stream流 核心的 Collectors工具类 更是封装了大量牛逼的方法,即丰富且强大!

在一般情况下来说,在处理业务数据时就没有 Stream 处理不了的,如果有,照着API与API之间的组合多试几次,相信也总还是能够得到你想要的结果的!

总而言之言而总之,Stream流真的非常强大!强大的同时且又十分优雅!