美文网首页
JDK1.8-Collectors方法介绍

JDK1.8-Collectors方法介绍

作者: 骑着乌龟去看海 | 来源:发表于2018-06-17 16:56 被阅读24次

    一、前言

      前文说到,Stream API中的collect方法,可以通过参数Collector 将流按照相应的规则转换为对应的集合类型,而Collector的帮助类Collectors提供了许多现成的静态方法来减少我们的操作。本文,我们就来了解下这些常用的方法。

    二、Collectors静态方法

    1. toCollection

    toCollection方法可以指定转换集合的类型:

    // 将Stream转换为HashSet集合
    HashSet<Integer> hashSet = Stream.of(1, 2, 4, 5).collect(Collectors.toCollection(HashSet::new));
    
    2. toSet/toList方法

    这两个方法比较简单,就是转为对应的集合对象。其中toList返回的是ArrayList,而toSet返回的是HashSet:

    List<Integer> list = Stream.of(1, 2, 4, 5).collect(Collectors.toList());
    Set<Integer> set = Stream.of(1, 2, 4, 5).collect(Collectors.toSet());
    
    3. joining方法

    将Stream流中的数据通过某个分隔符号,拼接成字符串。该方法共有三个重载方法,并且该方法要求Stream流中的对象是字符串类型:

    // 默认直接拼接
    public static Collector<CharSequence, ?, String> joining()
    // 添加分隔符进行拼接
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
    // 添加分隔符进行拼接,并且指定前后缀
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                                 CharSequence prefix,
                                                                 CharSequence suffix)
    

    方法比较简单,直接通过例子来看:

    // 没有分隔符,默认直接拼接,打印出  1234
    String join = Stream.of("1", "2", "3", "4").collect(Collectors.joining());
    // 使用逗号分隔符,打印出   1,2,3,4
    join = Stream.of("1", "2", "3", "4").collect(Collectors.joining(","));
    // 使用逗号分隔符,并且给最终拼接的字符串添加前后缀, 打印  [1,2,3,4]
    join =  Stream.of("1", "2", "3", "4").collect(Collectors.joining(",", "[","]"));
    
    4. groupingBy方法
    4.1 方法简介

    该方法用于对Stream流中某个属性进行分组,返回的类型是Map,同样,该方法也是有多个重载方法:

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

    这是最基础的groupingBy方法,传递单个对应的表达式即可,可以看到,这里调用了两个参数的groupingBy方法,并且返回的List默认调用的是toList方法,也就是返回的是ArrayList,再来看两个参数的方法:

    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方法,不过这里可以看到,groupBy返回的map默认是HashMap类型。

    4.2 例子

    接下来我们来看例子,我们新建Person对象:

    class Person {
        private String city;
        private String surname;
        private Integer sum;
        // get set,构造方法,toString省略
    }
    

    然后初始化数据:

    public static List<Person> initPerson() {
        return Arrays.asList(new Person("shenzhen", "张", 1),
                new Person("beijing", "王", 2),
                new Person("shanghai", "李", 3),
                new Person("beijing", "赵", 4));
    }
    

    然后,我们通过属性city进行分组:

    List<Person> list = initPerson();
    Map<String, List<Person>> map = list.stream().collect(Collectors.groupingBy(Person::getCity));
    System.out.println(map);
    

    分组生成的Map的key是我们分组的条件,这里是city属性,而Map的value默认是key相同的对象组成的List集合,然后看一下打印的结果:

    {shanghai=[Person{city='shanghai', surname='李', sum=3}], 
    shenzhen=[Person{city='shenzhen', surname='张', sum=1}], 
    beijing=[Person{city='beijing', surname='王', sum=2}, Person{city='beijing', surname='赵', sum=4}]}
    

    然后,如果我们分组后要统计map中各项value的某一项的总和,这里我们统计每个city中的城市人数,这时候我们就可以使用:

    Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity, 
            Collectors.summingInt(Person::getSum)));
    System.out.println(map);
    

    打印结果:

    {shanghai=3, shenzhen=1, beijing=6}
    

    紧接着,在此基础上,如果我们想指定返回的Map类型,比如说根据city进行排序的TreeMap,那么我们可以接着调用groupingBy方法的另一个重载方法:

    Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity, 
            TreeMap::new, Collectors.summingInt(Person::getSum)));
    System.out.println(map);
    

    最终打印结果:

    {beijing=6, shanghai=3, shenzhen=1}
    
    5. groupingByConcurrent方法

    由于groupingBy是非线程安全的,而该方法则是groupingBy方法的线程安全版本,默认情况下,返回的Map类型是ConcurrentHashMap,用法和groupingBy方法是类似的。简单看下方法声明:

    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)
    

    再来简单看下使用:

    list.stream().collect(Collectors.groupingByConcurrent(Person::getCity));
    list.stream().collect(Collectors.groupingByConcurrent(Person::getCity,
            ConcurrentSkipListMap::new, Collectors.summingInt(Person::getSum)));
    
    6. toMap方法

    将Stream流转换为Map对象。同样,该方法有三个重载方法,我们先来看最简单的方法:

    public static <T, K, U> Collector<T, ?, Map<K,U>> 
        toMap(Function<? super T, ? extends K> keyMapper,
              Function<? super T, ? extends U> valueMapper)
    

    该方法传递两个参数,分别是Map中key和value对应的函数式接口参数,我们来简单看一下例子,还借用上文的Person对象:

    public static List<Person> initPerson() {
        return Arrays.asList(new Person("shenzhen", "张", 1),
                new Person("beijing", "王", 2),
                new Person("shanghai", "李", 3));
    }
    

    然后,使用toMap方法:

    List<Person> list = initPerson();
    // 打印结果   {shanghai=3, shenzhen=1, beijing=2}
    Map<String, Integer> map = list.stream().collect(Collectors.toMap(Person::getCity, Person::getSum));
    

    当然,该方法是不允许Stream中对应的key有重复值的,如果有重复值,将直接抛出异常。而针对key中有重复值的情况,我们可以调用另外一个重载方法:

    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重复的时候,是使用新值还是旧值,或者指定额外的操作(比如新值与旧值的和):

    public static List<Person> initPerson() {
        return Arrays.asList(new Person("shenzhen", "张", 1),
                new Person("beijing", "王", 2),
                new Person("shanghai", "李", 3),
                new Person("beijing", "张", 4));
    }
    

    我们在例子中添加了一个重复的key,现在如果我们来使用上面这个方法来处理下key重复的问题:

    List<Person> list = initPerson();
    Map<String, Integer> map = list.stream().collect(
            Collectors.toMap(Person::getCity, Person::getSum, (f, s) -> f + s));
    System.out.println(map);
    

    可以看到,我们在第三个参数里通过lambda表达式指定了针对key重复的情况下要返回的值,lambda表达式的第一个代表旧值,第二个代表新值:

    // 使用旧的值,本例子中结果打印:{shanghai=3, shenzhen=1, beijing=2}
    (f, s) -> f   
    // 使用新的值替换旧的值,打印:{shanghai=3, shenzhen=1, beijing=4}
    (f, s) -> s 
    // 对新值和旧值进行处理,比如返回新值与旧值的和:{shanghai=3, shenzhen=1, beijing=6}
    (f, s) -> f + s
    

    如果我们不想使用默认的返回类型HashMap,可以通过toMap方法的最后一个参数来进行自定义返回类型:

    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) {
    

    来看下实现:

    List<Person> list = initPerson();
    Map<String, Integer> map = list.stream().collect(
            Collectors.toMap(Person::getCity, Person::getSum, (f, s) -> s + f, TreeMap::new));
    System.out.println(map);
    

    使用该重载方法,我们最终返回的对象定义为了有顺序的Map类型。

    6. toConcurrentMap方法

    这个方法就不多说了,toMap方法的线程安全版本。

    7. summarizingInt/summarizingLong/summarizingDouble方法

      这个方法比较简单,用于对Stream中的数值对象生成统计信息,返回值类型比如针对int的IntSummaryStatistics,我们前文已经了解过,包含了常用的操作:count,sum,max,min,average等操作:

    List<Person> list = initPerson();
    IntSummaryStatistics intSummaryStatistics = list.stream().collect(
            Collectors.summarizingInt(input -> input.getSum()));
    System.out.println(intSummaryStatistics.getSum());
    System.out.println(intSummaryStatistics.getMax());
    

    这其中要求对应的参数是数值类型,而针对long的LongSummaryStatistics和针对double类型的DoubleSummaryStatistics是类似的。

    8.summingInt/summingLong/summingDouble方法

    这三个方法更简单了,表示返回Stream中数值对象的总和,相当于上面三个方法返回值中的sum属性:

    Integer sum = list.stream().collect(Collectors.summingInt(Person::getSum));
    

    并且该方法也相当于IntStream的sum方法:

    Integer sum = list.stream().mapToInt(Person::getSum).sum();
    
    9. averagingInt/averagingLong/averagingDouble方法

    见名知义,这几个方法是为了获取平均值的:

    Double sum = list.stream().collect(Collectors.averagingInt(Person::getSum));
    

    这几个方法返回值都是Double类型。

    10. partitioningBy方法

      该方法也是用于分组,不过是根据某一条件进行分组,最终分成满足条件的true和不满足条件的false两个分组,返回类型是Map<Boolean, Object>,看下面的例子,比如我们根据Person中sum是否大于2来进行分组:

    Map<Boolean, List<Person>> map = list.stream().collect(
            Collectors.partitioningBy(input -> input.getSum() >= 2));
    System.out.println(map);
    

    最终打印:

    {false=[Person{city='shenzhen', surname='张', sum=1}], 
    true=[Person{city='beijing', surname='王', sum=2}, Person{city='shanghai', surname='李', sum=3}, Person{city='beijing', surname='张', sum=4}]}
    

    该方法还有一个重载方法,包含两个参数,我们可以通过第二个参数在上面分组基础上再进一步做处理:

    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream)
    

    比如我们再判断sum是否大于2的基础上,再判断city是否是beijing的:

    Map<Boolean, Map<Boolean, List<Person>>> map = list.stream().collect(
        Collectors.partitioningBy(input -> input.getSum() >= 2,
        Collectors.partitioningBy(input -> "beijing".equals(input.getCity()))));
    
    {false={false=[Person{city='shenzhen', surname='张', sum=1}], true=[]}, 
    true={false=[Person{city='shanghai', surname='李', sum=3}], true=[Person{city='beijing', surname='王', sum=2}, Person{city='beijing', surname='张', sum=4}]}}
    

    再比如说,我们在判断sum是否大于2的基础上,计算对应的个数:

    Map<Boolean, Long> map = list.stream().collect(
        Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.counting()));
    
    {false=1, true=3}
    

    该方法是非线程安全的。

    11. reducing方法

    该方法和Stream中的reduce方法功能类似,用来执行一些二目运算的操作,比如数值类型的求和,求最大值,求最小值,字符串连接,集合类型的交集并集等操作。同样,该方法也有多个重载方法,不过和Stream中的reduce方法差不多:

    Stream.of(1, 2, 4, 5).collect(Collectors.reducing(0, (x, y) -> x + y));
    // 等同于
    Stream.of(1, 2, 4, 5).reduce(0, (x, y) -> x + y);
    

    然后看下结合groupingBy方法使用:

    // 分组,保留组内key相同的最后一个
    Map<String, Optional<Person>> maxMap = list.stream().collect(Collectors.groupingBy(Person::getCity,
            Collectors.reducing((f,s) -> s)));
    System.out.println(maxMap);
    

    结合partitioningBy方法使用:

    // 根据条件过滤分组后,保留最大的
    Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
    Map<Boolean, Optional<Person>> maxBoolMap = list.stream().collect(
            Collectors.partitioningBy(input -> input.getSum() >= 2,
            Collectors.reducing(BinaryOperator.maxBy(sumComparator))));
    System.out.println(maxBoolMap);
    

    最终的打印结果:

    {shanghai=Optional[Person{city='shanghai', surname='李', sum=3}], 
    shenzhen=Optional[Person{city='shenzhen', surname='张', sum=1}], 
    beijing=Optional[Person{city='beijing', surname='张', sum=4}]}
    
    {false=Optional[Person{city='shenzhen', surname='张', sum=1}], 
    true=Optional[Person{city='beijing', surname='张', sum=4}]}
    

    其他重载方法和reduce重载的方法类似,不多说了。

    12. maxBy/minBy方法

    这两个方法就比较简单了,就是通过给定的比较器获取最小,最大元素:

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

    从源代码可以看出,其实这两个方法都是借助于reducing方法来实现的,那么上面reducing的例子我们也可以表示为:

    // 根据条件过滤分组后,保留最大的
    Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
    Map<Boolean, Optional<Person>> maxBoolMap = list.stream().collect(
            Collectors.partitioningBy(input -> input.getSum() >= 2,
            Collectors.minBy(sumComparator)));
    
    13. counting方法

    用于获取Stream流中满足条件的元素数量,我们来看一个前面已经接触过的例子:

    Long count = Stream.of(1, 2, 4, 5).collect(Collectors.counting());
    // 等价于
    Long count = Stream.of(1, 2, 4, 5).count();
    
    // 获取满足条件的元素的数量
    Map<Boolean, Long> maxBoolMap = list.stream().collect(
        Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.counting()));
    
    {false=1, true=3}
    
    14. collectingAndThen方法

    该方法接收两个参数,表示在第一个参数执行基础上,再执行第二个参数对应的函数表达式,我们来看几个例子:

    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    Double result = list.stream().collect(Collectors.collectingAndThen(Collectors.averagingInt(v -> v),
            s -> s * s));
    // output: 6.25
    System.out.println(result);
    

    该例子表示先对Stream的元素计算平均值,然后将平均值的平方返回,注意下返回值类型。再看一个例子:

    // 根据条件过滤分组后,获取最小的
    Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
    Map<Boolean, Person> maxBoolMap = list.stream().collect(
            Collectors.partitioningBy(input -> input.getSum() >= 2,
                    Collectors.collectingAndThen(Collectors.minBy(sumComparator), Optional::get)));
    System.out.println(maxBoolMap);
    

    最后看下官网的例子:

    // 生成不可变List
    List<Person> unmodifiableList = list.stream().collect(
            Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
    
    15. mapping方法

    mapping方法用于对Stream中元素的某个具体属性做进一步的映射处理,一般是和其他方法一起组合使用。我们来简单看一下例子:

    // 根据条件过滤分组后,获取组内元素的surname,并用逗号分割
    Map<String, String> nameByCity
            = list.stream().collect(Collectors.groupingBy(Person::getCity,
            Collectors.mapping(Person::getSurname, Collectors.joining(","))));
    
    // output:{shanghai=李, shenzhen=张, beijing=王,张}
    System.out.println(nameByCity);
    

    再看另一个例子:

    // 根据条件过滤分组后,获取组内元素的surname,并用逗号分割
    Map<String, Set<String>> nameByCity
            = list.stream().collect(Collectors.groupingBy(Person::getCity,
            Collectors.mapping(Person::getSurname, Collectors.toSet())));
    
    // output:{shanghai=[李], shenzhen=[张], beijing=[张, 王]}
    System.out.println(nameByCity);
    

    三、总结

    到这里,Collectors的静态方法基本上都学习过了,从上面这些例子可以看出,这些方法在某些条件下是可以互换,或者说实现方式不止一种。平时在工作中我们可以多尝试去使用,以加深对这些方法的了解。

    本文主要参考自官网API及源代码:https://docs.oracle.com/javase/8/docs/api/
    java.util.stream 库简介

    相关文章

      网友评论

          本文标题:JDK1.8-Collectors方法介绍

          本文链接:https://www.haomeiwen.com/subject/asvweftx.html