java8學習:用流收集數據

內容來自《 java8實戰 》,本篇文章內容均爲非盈利,旨爲方便自己查詢、總結備份、開源分享。如有侵權請告知,馬上刪除。
書籍購買地址:java8實戰

  • 下面我們將採用這樣一個實體類

    @Data
    public class Dish {
        private final String name;
        private final boolean vegetarian;   //是否是素食
        private final int calories;  //卡路里
        private final Type type;  //類型
        public enum Type{
            MEAT,FISH,OTHER;
        }
        public Dish(String name, boolean vegetarian, int calories, Type type) {
            this.name = name;
            this.vegetarian = vegetarian;
            this.calories = calories;
            this.type = type;
        }
    }  
    • 上面是一個菜單的一個實體類,下面我們添加到list中
    List<Dish> menu = Arrays.asList(
            new Dish("apple",true,50, Dish.Type.OTHER),
            new Dish("chicken",false,350, Dish.Type.MEAT),
            new Dish("rich",true,150, Dish.Type.OTHER),
            new Dish("pizza",true,350, Dish.Type.OTHER),
            new Dish("fish",false,250, Dish.Type.FISH),
            new Dish("orange",true,70, Dish.Type.OTHER),
            new Dish("banana",true,60, Dish.Type.OTHER));  
    • 好了到這就可以開始進行菜單中菜的分類了,那麼我們可以先按着原來的辦法,按着type分類,如下
    @Test
    public void test() throws Exception {
        Map<Dish.Type,List<Dish>> groupByType = new HashMap<>();
        for (Dish dish : menu) {
            Dish.Type type = dish.getType();
            List<Dish> dishes = groupByType.get(type);
            if (dishes == null){  //如果爲null說明第一次遇到某一個type
                dishes = new ArrayList<>();
                groupByType.put(type,dishes);
            }
            dishes.add(dish);
        }
        System.out.println(groupByType);
    }
    • 那麼我們用Stream中的collect方法來實現
    @Test
    public void test() throws Exception {
        Map<Dish.Type, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(menu -> menu.getType()));
        System.out.println("collect = " + collect);
    }
    • 到這我們就知道了collect能給我們帶來的便捷,下面將具體介紹collect的使用,以及自定義一個collect能接受的參數
  • 使用

    • 求上面菜單中有多少個菜
    Long collect = menu.stream().collect(Collectors.counting());
    //更好的做法
    long count = menu.stream().count();  
    • 查找最大和最小的卡路里
    //既然是找出最大和最小的卡路里,那麼肯定是有比較器的,比較器比較Dish中的卡路里屬性
    //最大
    Comparator<Dish> comparator = Comparator.comparing(Dish::getCalories);
    Optional<Dish> collect = menu.stream().collect(Collectors.maxBy(comparator));
    //對於返回Option很正常,萬一比較列表沒有值呢?所以會返回一個Option容器
    //最小
    Optional<Dish> collect1 = menu.stream().collect(Collectors.minBy(comparator));
    • 彙總:summingInt(),可接收一個把對象映射爲求和所需int的函數,並返回收集器,(也就是說求和操作)
    //計算卡路里總和
    Integer collect = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
    System.out.println(collect);
    //下面的更好一些,避免了拆箱裝箱操作
    int sum = menu.stream().mapToInt(Dish::getCalories).sum();
    System.out.println("sum = " + sum);
    //當然有summingInt就會有summintLong,summingDouble
    • 求卡路里平均數
    Double collect = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
    • 還有一個方法的返回值包含上面所有的信息:summarizingInt()
    IntSummaryStatistics collect = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
    System.out.println("collect = " + collect);
    //輸出如下,包含最大值最小值等信息
    輸出:collect = IntSummaryStatistics{count=7, sum=1280, min=50, average=182.857143, max=350}
    //想訪問裏面的任一屬性只要通過get方法即可,比如:getMin();
    • 連接字符串
    //將菜名全部連接起來
    String collect = menu.stream().map(Dish::getName).collect(Collectors.joining());
    //collect = applechickenrichpizzafishorangebanana
    System.out.println("collect = " + collect);
    //上面的結果看不清楚,join也提供了重載的方法,可以加入分隔符
    String collect1 = menu.stream().map(Dish::getName).collect(Collectors.joining(","));
    //collect1 = apple,chicken,rich,pizza,fish,orange,banana
    System.out.println("collect1 = " + collect1);
    String collect2 = menu.stream().map(Dish::getName).collect(Collectors.joining(",","[","]"));
    //collect2 = [apple,chicken,rich,pizza,fish,orange,banana]
    System.out.println("collect2 = " + collect2);
    • ruducing
    //我們可以使用reducing方法來實現之前的求和操作
    //0作爲初始值,Dish::getCalories每次要執行的方法會返回一個值,Integer::sum對返回的值進行的操作
                                                              //初始值   轉換函數        累計函數
    Integer collect = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));
    //實現上面求卡路里最大值
    //可以將下面的一個參數的reducing看做是上面三個參數的特殊形式,它把流中的第一個項目作爲起點,返回值是輸入的參數dish1或dish2,所以如果輸入參數
    //不存在也會有Option
    Optional<Dish> collect = menu.stream().collect(Collectors.reducing(((dish1, dish2) -> dish1.getCalories() > dish2.getCalories() ? dish1 : dish2)));
    • 我們前面說的reduce和現在說的collect有什麼區別?

      • reduce方法旨在把兩個值結合起來生成一個新值,他是一個不可變的歸約
      • collect方法的設計就是要改變容器,從而累計要輸出的結果
    • reducing測試
    menu.stream()
          .collect(Collectors.reducing(((dish1, dish2) -> dish1.getName() + dish2.getName())));
    //這段代碼可以通過編譯嗎?
    //不可以因爲reducing需要一個BinaryOperator,而它的定義如下
    public interface BinaryOperator<T> extends BiFunction<T,T,T>
    //如此可以看出它傳入的TT返回也需要是T類型,所以我們傳入Dish返回的也應該是Dish類型
  • 分組

    • 其實前面第一個例子我們應使用了分組了,那就是根據菜單中的type進行分組
    //我們給groupingBy方法傳遞了一個FUnction,它提取了流中每一道Dish的type,然後根據type分組,我們把傳入的Function稱爲分類函數,因爲他用來把流中的元素分成不同的組
    //map的key就是type類型,value就是屬於type的所有Dish
    Map<Dish.Type, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType));
    • 但是如果我們的分類條件並不一定是方法引用的返回值呢?比如我們要卡路里> 120的和小於120的,那該怎麼分?
    @Test
    public void test() throws Exception {
        Map<Integer, List<Dish>> collect = menu.stream()
                .collect(Collectors.groupingBy(dish -> {
                    if (dish.getCalories() <= 120) return 1;   //只需要區別開就好
                    else return 2;//只是區別開就好
                }));
        System.out.println("collect = " + collect);
    }
    //輸出:collect = {1=[Dish(name=apple, vegetarian=true, calories=50, type=OTHER), Dish(name=orange, vegetarian=true, calories=70, type=OTHER), Dish(name=banana, vegetarian=true, calories=60, type=OTHER)],
    // 2=[Dish(name=chicken, vegetarian=false, calories=350, type=MEAT), Dish(name=rich, vegetarian=true, calories=150, type=OTHER), Dish(name=pizza, vegetarian=true, calories=350, type=OTHER), Dish(name=fish, vegetarian=false, calories=250, type=FISH)]}
    • 當然返回1和2是不清楚的,如果大於120算是高卡路里,否則就是低卡路里,那麼就可以定義一個枚舉,然後返回枚舉值加以切分就好了
  • 多級分組

    • 如果我們不止只想分一層,比如我們要按是否是素食和肉食分組,然後再按卡路里是否<=60分組,這次我們不返回1和2,採用枚舉返回,那麼該怎麼做
    enum MyEnum{YES,NO}
    @Test
    public void test() throws Exception {
        Map<Boolean, Map<MyEnum, List<Dish>>> collect = menu.stream().collect(Collectors.groupingBy(Dish::isVegetarian, Collectors.groupingBy(dish -> {
            if (dish.getCalories() <= 60) return MyEnum.YES;
            else return MyEnum.NO;
        })));
        System.out.println(collect);
    }
    //{false={NO=[Dish(name=chicken, vegetarian=false, calories=350, type=MEAT), Dish(name=fish, vegetarian=false, calories=250, type=FISH)]},
    // true={YES=[Dish(name=apple, vegetarian=true, calories=50, type=OTHER), Dish(name=banana, vegetarian=true, calories=60, type=OTHER)],
    // NO=[Dish(name=rich, vegetarian=true, calories=150, type=OTHER), Dish(name=pizza, vegetarian=true, calories=350, type=OTHER), Dish(name=orange, vegetarian=true, calories=70, type=OTHER)]}}
  • 按group收集數據

    • 上面多級分組我們看到可以把第二個groupingby收集器傳遞給外層收集器來實現多級分組。傳遞給第一個groupingby的第二個收集器可以是任何類型,而不一定還是一個groupingby
    • 求每種(type)菜的個數
    Map<Dish.Type, Long> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
    //collect = {MEAT=1, FISH=1, OTHER=5}
    System.out.println("collect = " + collect);
    • 求每組最高卡路里的dish
    @Test
    public void test() throws Exception {
        Map<Dish.Type, Optional<Dish>> collect = menu.stream()
                .collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
        System.out.println("collect = " + collect);
        //collect = {MEAT=Optional[Dish(name=chicken, vegetarian=false, calories=350, type=MEAT)],
        // OTHER=Optional[Dish(name=pizza, vegetarian=true, calories=350, type=OTHER)],
        // FISH=Optional[Dish(name=fish, vegetarian=false, calories=250, type=FISH)]}
    }
    • 對於上面的代碼,Map的第二個泛型是Option類型的,但是我們可以想到,如果menu中沒有對應的type,那麼根本就不可能到maxBy方法讓其返回Option,所以在這的Option並不是很有用,反而影響了我們的查看和使用。那麼我們就可以把它去掉
    Map<Dish.Type, Dish> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType,
            Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
    System.out.println("collect = " + collect);
    //collect = {OTHER=Dish(name=pizza, vegetarian=true, calories=350, type=OTHER),
    // FISH=Dish(name=fish, vegetarian=false, calories=250, type=FISH),
    // MEAT=Dish(name=chicken, vegetarian=false, calories=350, type=MEAT)}
    • 對照上面,我們發現已經去掉了Option的包裝,我們是用的Collectors.collectingAndThen方法,此方法接收兩個參數:要轉換的收集器和轉換函數,首先他會找出最大的卡路里然後再將這個最大的卡路里對象進行轉換:Option::get。所以我們的輸出結果中就去掉了Optional
    • 一些其他與groupingby使用的例子
    • 求每組的卡路里總和
    Map<Dish.Type, Integer> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.summingInt(Dish::getCalories)));
    //{OTHER=680, MEAT=350, FISH=250}
    • 和mapping組合使用
    public void test() throws Exception {
        Map<Dish.Type, Set<Integer>> collect = menu.stream().collect(
                Collectors.groupingBy(Dish::getType, Collectors.mapping(dish -> {
                    if (dish.getCalories() <= 120) return 1;
                    else return 2;
                }, Collectors.toSet())));
        System.out.println("collect = " + collect);
    }
    //collect = {MEAT=[2], FISH=[2], OTHER=[1, 2]}
  • 分區

    • 分區是分組的一種特殊情況:使用一個Predicate函數作爲分類函數,所以分區就只能分爲true和false區
    //分開素食和肉食
    Map<Boolean, List<Dish>> collect = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
    //然後我們通過collect.get(true)就能找到素食了
    //當然用filter過濾也可以只不過是只能過濾是素食或者是肉食的Dish了
  • 分區的優勢

    • 分區的好處就比如上面代碼演示了,filter只能保留一個結果的Dish,要不是素食的要麼不是素食的,而分區就都能保留下來
    • 分區的重載方法可以傳入一個groupingby進行區內分組
    Map<Boolean, Map<Dish.Type, List<Dish>>> collect = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian, Collectors.groupingBy(Dish::getType)));
    //根據是否是素食分區後,再將每個區內細分爲各個類型  
    • 分區後的第二個參數也可以再次傳入一個分區進行二次分區
    • 記住分區內只能傳入返回boolean值的表達式,否則無法通過編譯
  • 收集器接口Collector

    • 此接口爲實現具體的歸約操作提供了範本,之前的toList或者groupingby都是此接口實現的,自己也可以通過這個接口自定義歸約實現
    • 接口定義
    public interface Collector<T, A, R> {
      /**
        * 建立新容器
        * 返回一個Supplier,他是用來創建一個空的累加器的實例,共數據收集過程使用
        */
       Supplier<A> supplier();
    
       /**
        * 將元素添加到結果容器
        * 會返回執行歸約操作的函數,他就是將元素處理後添加到累加器的,他會有兩個參數,一個是累加器,一個是元素本身
        */
       BiConsumer<A, T> accumulator();
    
       /**
        * 對結果容器應用最終轉換
        * 必須返回在累加過程的最後要調用的一個函數,以便將累加器對象轉換爲整個集合操作的最終結果
        * 如果accumulator方法操作完之後已經符合期待類型,那麼此方法可以原樣返回不做操作
        */
       Function<A, R> finisher();
    
       /**
        * 合併兩個結果容器:用於並行
        * 會返回一個供歸約操作使用的函數,定義了對流的各個子部分進行並行處理時,各個子部分歸約所得的累加器要如何合併
        */
       BinaryOperator<A> combiner();
    
       /**
        * 類似Spliterator中的characteristics方法
        * 會返回一個不可變的Characteristics集合,它定義了收集器的行爲
        * 尤其是關於流是否可以並行歸約,以及可以使用那些優化的提示
        * 包括三個枚舉
        *      CONCURRENT:accumulator方法可以從多個線程同事調用,且該收集器可以並行歸約流,如果收集器沒有標爲UNORDERED,那麼它僅在用於無序數據源時纔可以並行歸約
        *      UNORDERED:歸約結果不受流中項目的遍歷和累積順序的影響
        *      IDENTITY_FINISH:累計器對象將會直接用作歸約過程的最終結果,也就意味着,將累加器A不加檢查的轉換爲結果R是安全的
        */
       Set<Characteristics> characteristics();
        ....
      }
    • T是流中要收集的項目的泛型
    • A是累加器的類型,累加器是在手機過程中用於累積部分結果的對象
    • R是收集操作得到的對象的類型(收集器返回值類型)
  • 實現一個類似toList()方法的功能

    public class MyToList<T> implements Collector<T, List<T>, List<T>> {
        @Override
        public Supplier<List<T>> supplier() {
            //創建一個list作爲累加器供以後使用
            return ArrayList::new;
        }
        @Override
        public BiConsumer<List<T>, T> accumulator() {
            //每次傳入累加器和元素,然後把元素add到累加器
            //也可以寫做 return (list,t) -> list.add(t);
            return List::add;
        }
        @Override
        public BinaryOperator<List<T>> combiner() {
            //如果是並行的,那麼這就是將累加器合併的操作
            return (l1,l2) -> {
                l1.addAll(l2);
                return l1;
            };
        }
        @Override
        public Function<List<T>, List<T>> finisher() {
            //因爲我們要實現的功能就是將值放入List,現在的累加器正好是我們所需要的List類型,所以直接返回就好啦
            //對於下面這個identity方法的解釋
            //Returns a function that always returns its input argument
            //輸入進來的在返回出去
            return Function.identity();
        }
        //
        @Override
        public Set<Characteristics> characteristics() {
            //IDENTITY_FINISH:累計器對象將會直接用作歸約過程的最終結果,因爲我們不需要轉換爲其他類型的結果
            //CONCURRENT:可以從多個線程同事調用,且該收集器可以並行歸約流,但是沒有標爲UNORDERED,那麼它僅在用於無序數據源時纔可以並行歸約
            return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH,Characteristics.CONCURRENT));
        }
    }
  • 進行自定義收集而不去實現Collector

    • 對於IDENTITY_FINISH的收集操作,還有一種辦法可以得到同樣的結果而無需從頭實現新的Collectors接口
    • Stream有一個重載的collect方法可以接受另外三個方法-->supplier,accumulator,combiner,也就是說不用實現自己的類而是直接把實現邏輯傳入固定的參數位置就能夠使用,比如把Name收集到List
    ArrayList<Object> collect = menu.stream().map(Dish::getName).collect(
            ArrayList::new,//創建累加器
            List::add,//對每個元素實現的累加器操作
            List::addAll);//並行組合累加器的操作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章