likes
comments
collection
share

Collectors 工具类怎样用?看完这篇文章,或许你就懂了~

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

完整的文章往这跳👉:从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!

Stream流的方法往这跳👉:Stream流的方法很多?基本上都在这里了

2.5 Collectors 工具类

Collectors 工具类怎样用?看完这篇文章,或许你就懂了~

通过这个图片可以看到,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

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

Collectors 工具类怎样用?看完这篇文章,或许你就懂了~

是不是又清晰明了了?没错,该方法返回的是对数据的一个分析整合,包括 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 类型


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

Collectors 工具类怎样用?看完这篇文章,或许你就懂了~

  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 = 曼谷--莫斯科--圣披德堡--香港--澳门--台湾--伦敦

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

转载自:https://juejin.cn/post/7222902032947642424
评论
请登录