美文网首页
Java8中流的归约

Java8中流的归约

作者: suikaJY | 来源:发表于2018-11-13 17:31 被阅读0次

    1. 收集器简介

    收集器就是流的终端操作操作类,常用的就是collect操作(当然还有其他的),这个操作的参数就是一个收集器,他有很多官方定义好的用法,下文会一一介绍。

    @收集器示意图

    collect操作符一般都接受Collectors中定义的归约方法:

    List<Transaction> transactions =
        transactionStream.collect(Collectors.toList());
    

    2. 归约和汇总

    归约为一个整数——流中所有元素个数:

    long howManyDishes = menu.stream().collect(Collectors.counting());
    

    还可以更简洁:

    long howManyDishes = menu.stream().count();
    

    查找流中最小值和最大值

    Comparator<Dish> dishCaloriesComparator =
            Comparator.comparingInt(Dish::getCalories);
    Optional<Dish> mostCalorieDish =
            menu.stream()
                    .collect(maxBy(dishCaloriesComparator));
    

    汇总

    对流中对象中的某个字段求和的归约操作叫做汇总操作

    int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
    
    @summingInt的工作流程

    汇总也指求某一字段的平均数:

    double avgCalories =
        menu.stream().collect(averagingInt(Dish::getCalories));
    

    求平均值、最大值等所有计算结果:

    IntSummaryStatistics menuStatistics =
        menu.stream().collect(summarizingInt(Dish::getCalories));
    

    这个IntSummaryStatistics类的getter可以获取五个计算结果:

    IntSummaryStatistics{count=9, sum=4300, min=120,
    average=477.777778, max=800}
    

    同样,有Int前缀的特化通常就有Long和Double特化。这里不赘述。

    连接字符串

    连接所有菜肴名字的joining操作:

    String shortMenu = menu.stream().map(Dish::getName).collect(joining());
    

    如果对对象使用joining那么就会调用对象的toString

    String shortMenu = menu.stream().collect(joining());
    

    joining还可以添加分隔符:

    String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
    

    广义的归约汇总

    上述归约方法基本能满足程序员生理需求,但如果想定义更复杂的归约规则还是要用到reduce操作:

    int totalCalories = menu.stream().collect(reducing(
        0, Dish::getCalories, (i, j) -> i + j));
    

    同样reduce也有方便的重载:

    Optional<Dish> mostCalorieDish =
        menu.stream().collect(reducing(
        (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
    

    它相当于:

    reduce(0, Function.identity(), (Dish, Dish)-> Dish)
    

    3. 分组

    按类型将菜肴分组:

    Map<Dish.Type, List<Dish>> dishesByType =
        menu.stream().collect(groupingBy(Dish::getType));
    

    下面是输出:

    {FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
    MEAT=[pork, beef, chicken]}
    
    @分组流程图

    分组不一定非要使用对象的属性,也可以使用如下这种lambda:

    public enum CaloricLevel { DIET, NORMAL, FAT }
    
    Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
        groupingBy(dish -> {
            if (dish.getCalories() <= 400) return CaloricLevel.DIET;
            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT;
        } ));
    

    多级分组

    Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
            menu.stream().collect(
                    groupingBy(Dish::getType,
                            groupingBy(dish -> {
                                if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                                else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                                else return CaloricLevel.FAT;
                            })
                    )
            );
    
    @多级映射和n维表是等价的

    按子组收集数据

    其实就是对每个分组进行归约:

    Map<Dish.Type, Long> typesCount = menu.stream().collect(
        groupingBy(Dish::getType, counting()));
    // output: {MEAT=3, FISH=2, OTHER=4}
    

    groupingBy(f)其实是groupingBy(f, toList())的简便写法,所以后续的归约对前文的归约方法都适用。

    获取类菜中最高卡路里:

    Map<Dish.Type, Optional<Dish>> mostCaloricByType =
            menu.stream()
                    .collect(groupingBy(Dish::getType,
                            maxBy(comparingInt(Dish::getCalories))));
    

    这里返回结果是一个Optional<Dish>这其实并不太实用,所以Java8还有更简便的写法。

    把收集器转换成另一种类型
    Map<Dish.Type, Dish> mostCaloricByType =
            menu.stream()
                    .collect(groupingBy(Dish::getType,
                            collectingAndThen(
                                    maxBy(comparingInt(Dish::getCalories)),
                                    Optional::get)));
    
    分组还能干很多事

    分组求和

    Map<Dish.Type, Integer> totalCaloriesByType =
        menu.stream().collect(groupingBy(Dish::getType,
        summingInt(Dish::getCalories)));
    

    4. 分区

    分区是分组的一个特殊情况,只区分一判断式:

    Map<Boolean, List<Dish>> partitionedMenu =
            menu.stream().collect(partitioningBy(Dish::isVegetarian));
    
    // output: {false=[pork, beef, chicken, prawns, salmon],
    // true=[french fries, rice, season fruit, pizza]}
    

    分区的作用

    上面的代码乍一看并没有什么鸟用,直接filter一下也可以,但分区毕竟也是一种分组,如果加入多级分组的分区就看起来有那么一点鸟用了:

    Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
            menu.stream().collect(
                partitioningBy(Dish::isVegetarian,
                    groupingBy(Dish::getType)));
    

    总之这就相当于SQL语句中的筛选条件,结合各种归约可以进行灵活应用。

    Collectors类的静态工厂方法

    工厂方法 返回类型 用途
    toList List<T> 把流中所有项目收集到一个 List
    toSet Set<T> 把流中所有项目收集到一个 Set,删除重复项
    toCollection Collection<T> 把流中所有项目收集到给定的供应源创建的集合
    counting Long 计算流中元素的个数
    summingInt Integer 对流中项目的一个整数属性求和
    averagingInt Double 计算流中项目 Integer 属性的平均值
    summarizingInt IntSummaryStatistics 收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值
    joining String 连接对流中每个项目调用 toString 方法所生成的字符串
    maxBy Optional<T> 一个包裹了流中按照给定比较器选出的最大元素的 Optional,或如果流为空则为 Optional.empty()
    minBy Optional<T> 一个包裹了流中按照给定比较器选出的最小元素的 Optional,或如果流为空则为 Optional.empty()
    reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值
    collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果应用转换函数
    groupingBy Map<K, List<T>> 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键
    partitioningBy Map<Boolean,List<T>> 根据对流中每个项目应用谓词的结果来对项目进行分区

    2. 收集器接口

    收集器接口定义如下:

    public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
    }
    

    三个泛型定义如下:

    • T是流中收集项目的泛型(一般就是Stream的尖括号里那个)
    • A是累加器类型,求和就是int,打包成集合就是List<T>
    • R是收集操作最终返回的类型
      比如toList()对应的实现类ToListCollector<T>的声明如下:
    public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
    

    理解Collector接口声明的方法

    要想自定义一个Collector一般就是实现如下四个方法:

    1. 建立新的结果容器:supplier方法
    2. 将元素归约到第一步的结果容器:accumulator方法
    3. 将归约好的最终结果进行转换:finisher方法
    4. 合并两个结果容器:combiner方法
      @归约的逻辑步骤
    @使用combiner方法来并行化归约过程
    在Collector还有最后一个方法,characteristics。它用来定义收集器的行为,关于流是否可以并行归约,以及哪些可以优化的提示。Characteristics是一个包含三个项目的枚举:
    • UNORDERED:归约结果不受遍历顺序和累计顺序影响
    • CONCURRENT:在accumulator时是否可以多个线程同时调用。如果没有标为UNORDERED则只会在无序流上使用并行。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
    • IDENTITY_FINISH:这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
    成型后的ToListCollector
    public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
        @Override
        public Supplier<List<T>> supplier() {
            return ArrayList::new;
        }
        @Override
        public BiConsumer<List<T>, T> accumulator() {
            return List::add;
        }
        @Override
        public Function<List<T>, List<T>> finisher() {
            return Function.indentity();
        }
        @Override
        public BinaryOperator<List<T>> combiner() {
            return (list1, list2) -> {
                list1.addAll(list2);
                return list1;
            };
        }
        @Override
        public Set<Characteristics> characteristics() {
            return Collections.unmodifiableSet(EnumSet.of(
                    IDENTITY_FINISH, CONCURRENT));
        }
    }
    

    一种不需要实现Collector接口的自定义收集

    toList方法的另一种写法

    List<Dish> dishes = menuStream.collect(
            ArrayList::new,
            List::add,
            List::addAll);
    

    这种方式其实很方便,但没那么易读,实现个Collector接口也容易扩展。

    6. 开发你自己的收集器以获得更好的性能

    自定义Collector:

    package com.suikajy.java8note.ch6_collect;
    
    import java.util.*;
    import java.util.function.*;
    import java.util.stream.Collector;
    
    import static java.util.stream.Collector.Characteristics.*;
    
    public class PrimeCollector implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
    
        @Override
        public Supplier<Map<Boolean, List<Integer>>> supplier() {
            return () -> {
                Map<Boolean, List<Integer>> map = new HashMap<>();
                map.put(true, new ArrayList<>());
                map.put(false, new ArrayList<>());
                return map;
            };
        }
    
        @Override
        public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
            return (map, i) -> map
                    .get(isPrime(map.get(true), i))
                    .add(i);
        }
    
        @Override
        public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
            return (map1, map2) -> {
                map1.get(true).addAll(map2.get(true));
                map2.get(false).addAll(map2.get(false));
                return map1;
            };
        }
    
        @Override
        public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
            return Function.identity();
        }
    
        @Override
        public Set<Characteristics> characteristics() {
            return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
        }
    
        private boolean isPrime(List<Integer> acc, Integer candidate) {
            int candidateRoot = (int) Math.sqrt(candidate);
            return takeWhile(acc, i -> i < candidateRoot)
                    .stream()
                    .noneMatch(i -> candidate % i == 0);
    
    //        return acc.stream()
    //                .filter(i -> i < candidateRoot)
    //                .noneMatch(i -> candidate % i == 0);
        }
    
        // 使用takeWhile来加速filter。因为filter会对流中的每一个数字判断,但其实到了候选数字的根即可停止了。
        private static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
            int i = 0;
            for (A item : list) {
                if (!p.test(item)) {
                    return list.subList(0, i);
                }
                i++;
            }
            return list;
        }
    }
    
    

    主函数:

    public static void main(String[] args) {
        //IntStream.rangeClosed(2, 100).filter(i -> {
        //    int iRoot = (int) Math.sqrt(i);
        //    return IntStream.rangeClosed(2, iRoot)
        //            .noneMatch(divider -> i % divider == 0);
        //}).forEach(System.out::println);
    
        IntStream.rangeClosed(2, 100).boxed()
                .collect(new PrimeCollector())
                .get(true)
                .forEach(System.out::println);
    }
    

    7. 小结

    • collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。
    • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
    • 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。
    • 收集器可以高效地复合起来,进行多级分组、分区和归约。
    • 你可以实现Collector接口中定义的方法来开发你自己的收集器。

    整理自《Java 8 实战》

    相关文章

      网友评论

          本文标题:Java8中流的归约

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