4. JDK8的集合流

之前寫了JDK8集合流的入門以及篩選,也就是集合流使用的打開和中間操作。這次帶來的是不同的收集數(shù)據(jù)的方式。

本節(jié)代碼GitHub地址:https://github.com/WeidanLi/Java-jdk8-collect

一、準備:

還是老規(guī)矩,使用菜單進行示例。(代碼的話建議拷貝這部分)

二、收集器簡介

收集器即收集東西的容器,它用于使用集合流的時候的終端操作,即我們在日常的業(yè)務(wù)邏輯中把流進行過濾也好,進行篩選也好,然后我們總該要有一個容器可以存放這些過濾后的元素。這時候的收集器就派上用場了。如代碼所示一個最簡單的收集器的使用實例(當(dāng)然我感覺平時應(yīng)該沒人這么無聊)

 /**
 * 收集器是可以收集一個流中的數(shù)據(jù)的一個容器
 * cn.liweidan.collect.Demo01#demo01
 */
@Test
public void demo01(){
    List<Dish> collect = dishList.stream().collect(Collectors.toList());
    System.out.println(collect);
}

三、JDK8提供的預(yù)定義收集器

官方為我們提供了預(yù)定義的收集器的一些常用的必要功能,分別有:

  • 元素規(guī)約與匯總
  • 元素分組
  • 元素分區(qū)

1. 計算元素的個數(shù)counting()

counting()用于總結(jié)流中元素的個數(shù),有兩種寫法,分別如代碼所示。counting()使用起來還是比較簡單的。

/**
 * cn.liweidan.collect.Demo01#demo02()
 * counting()使用
 */
@Test
public void demo02(){
    /* 查詢卡路里大于400的菜單的個數(shù) */
    Long count = dishList.stream().filter(dish -> dish.getColories() > 400).collect(Collectors.counting());
    System.out.println("卡路里大于400的菜單個數(shù):" + count);
    
    /* 第二種寫法 */
    count = dishList.stream().filter(dish -> dish.getColories() > 400).count();
    System.out.println("卡路里大于400的菜單個數(shù):" + count);
}

2.查找流中的元素某個屬性的最大值或者最小值

我們通常需要去拿到一個對象集合中對象某個屬性進行查詢最大和最小值,按照JDK8以前的寫法是需要先去遍歷集合中所有的對象,去讀取某個屬性的值,然后去記錄最大值或者最小值進行記錄,全部遍歷完成以后把該值進行返回。這種寫法,描述起來也麻煩,寫起來也麻煩。

JDK8的流中就可以比較方便的拿到上面的需求。只需要在流中定義需要什么東西,當(dāng)流完成以后就可以取到所需要的值了。不過我們需要先定義一個比較器,來告訴JVM我們需要個什么值。

/**
 * cn.liweidan.collect.Demo01#demo03()
 * 取出最大值以及最小值
 */
@Test
public void demo03(){
    /* 定義一個卡路里比較器 */
    Comparator<Dish> comparator = Comparator.comparingInt(Dish::getColories);
    /* Collectors.maxBy(comparator)即取出流中的最大值 */
    Optional<Dish> collect = dishList.stream().collect(Collectors.maxBy(comparator));
    System.out.println(collect.get());
    /* Collectors.minBy(comparator)即取出流中的最小值 */
    Optional<Dish> collect1 = dishList.stream().collect(Collectors.minBy(comparator));
    System.out.println(collect1.get());
}

不過有時候我們需要最大值以及最小值在一個流中取出,這時候我們可以使用后面的分組進行實現(xiàn)。

4. 匯總

匯總即對集合中元素的某個屬性進行統(tǒng)計,如菜單中的所有卡路里的匯總。

/**
 * cn.liweidan.collect.Demo01#demo04()
 * 匯總:對集合中所有菜單的卡路里進行統(tǒng)計計算
 */
@Test
public void demo04(){
    int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
    System.out.println(collect);
}

示例中我們使用了summingInt方法,當(dāng)然Collectors還提供了Long和Double方法對Long和Double進行統(tǒng)計。

匯總不僅僅包括sum,還包括了平均數(shù)、最大值最小值等等。Collectors同時還定義了averagingInt以及IntSummaryStatistics來分別拿出元素屬性的均值和所有統(tǒng)計數(shù)據(jù)(包括最大值、最小值、均值等)

/**
 * 查詢菜單集合中卡路里的平均值以及所有統(tǒng)計數(shù)據(jù)
 */
@Test
public void demo05(){
    /* 查詢所有菜單卡路里的平均值 */
    Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
    System.out.println("卡路里均值:" + collect);
    /* 查詢菜單中所有的匯總數(shù)據(jù) */
    IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
    System.out.println(collect1);// IntSummaryStatistics{count=9, sum=4200, min=120, average=466.666667, max=800}
}

5. joining連接字符串

joining方法可以把自動調(diào)用對象的toString方法,然后把字符串連接在一起,如果需要使用分隔符,只要把分隔符傳遞給該方法就可以了。

/**
 * joining連接字符串
 */
@Test
public void demo06(){
    String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
    System.out.println(collect);
    // pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
}

6. 實現(xiàn)自定義歸約--reduce使用

像蚊帳之前講的均值、最值這些操作,其實都是官方對reduce常用方法的封裝,如果官方提供的這些方法不能夠滿足要求的話,那么就需要我們自己來自定義reduce的實現(xiàn)了。

reduce需要傳入三個參數(shù):

  • 第一個參數(shù)是規(guī)約操作的起始值,即如果要統(tǒng)計總值的時候,那么起始值是0

  • 第二個參數(shù)就是要調(diào)用的對象的方法了,即菜單的卡路里值

  • 第三個參數(shù)就是一個BinaryOperator操作了,在這里定義我們拿到的值的操作方式。即相加。

  • 也可以直接只傳需要的操作,去除前兩個參數(shù)。

/**
 * cn.liweidan.collect.Demo01#demo07()
 * Collectors.reducing的使用
 */
@Test
public void demo07(){
    /**
     * 取出卡路里最大的菜單
     */
    Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
    System.out.println(collect.get());

    /**
     * 計算菜單總卡路里值
     */
    Integer integer1 = dishList.stream().collect(Collectors.reducing(0,// 初始值
            Dish::getColories,// 轉(zhuǎn)換函數(shù)
            Integer::sum));// 累積函數(shù)
    System.out.println(integer1);

    Integer integer2 = dishList.stream().map(Dish::getColories).reduce(Integer::sum).get();
    System.out.println(integer2);

    int sum = dishList.stream().mapToInt(Dish::getColories).sum();// 推薦
    System.out.println(sum);

}

在計算總和的時候,推薦使用mapToInt,因為可以免去自動裝箱拆箱的性能消耗。

四、分組

1. 簡單分組

我們經(jīng)常需要對數(shù)據(jù)進行分組,特別是在數(shù)據(jù)庫操作的時候。當(dāng)我們需要從一個集合中進行分組,代碼會變得十分復(fù)雜,分組功能剛好能夠解決這個問題。我們可以對菜單中的類型進行分組,也可以根據(jù)卡路里的大小對菜單進行自定義的分組。

/**
 * 簡單分組
 */
@Test
public void test01(){
    /** 按照屬性類型進行分組 */
    Map<Dish.Type, List<Dish>> collect = dishList.stream().collect(Collectors.groupingBy(Dish::getType));
    System.out.println(collect);
    // {FISH=[Dish(name=prawns, vegetarain=false, colories=300, type=FISH),
    // Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // OTHER=[Dish(name=french fries, vegetarain=true, colories=530, type=OTHER),
    // Dish(name=rice, vegetarain=true, colories=350, type=OTHER),
    // Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER),
    // Dish(name=pizza, vegetarain=true, colories=550, type=OTHER)],
    // MEAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT),
    // Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=chicken, vegetarain=false, colories=400, type=MEAT)]}

    /** 自定義簡單的分組方式 */
    Map<CaloricLevel, List<Dish>> map = dishList.stream().collect(Collectors.groupingBy(d -> {
        /** 此處寫if的時候注意要顧及到所有的情況 */
        if(d.getColories() <= 400){
            return CaloricLevel.DIET;
        }else if (d.getColories() <= 700){
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    }));
    System.out.println(map);
    // {FAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT)],
    // NORMAL=[Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=french fries, vegetarain=true, colories=530, type=OTHER), Dish(name=pizza, vegetarain=true, colories=550, type=OTHER), Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // DIET=[Dish(name=chicken, vegetarain=false, colories=400, type=MEAT), Dish(name=rice, vegetarain=true, colories=350, type=OTHER), Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER), Dish(name=prawns, vegetarain=false, colories=300, type=FISH)]}
}

2. 多級分組

如果我們需要進行多級分組,比如根據(jù)菜單的類型分組的情況下又要根據(jù)卡路里大小進行分組。那么我們可以在groupingBy中再傳入第二個groupingBy。

/**
 * 多級分組
 */
@Test
public void test02(){
    Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = 
            dishList.stream().collect(Collectors.groupingBy(Dish::getType, 
                    Collectors.groupingBy(d -> {
        if (d.getColories() <= 400) {
            return CaloricLevel.DIET;
        } else if (d.getColories() <= 700) {
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    })));
    System.out.println(collect);
}

4. 按子組收集數(shù)據(jù)

在上一節(jié)的第二個參數(shù)傳遞的是一個groupingBy,但是收集器的第二個參數(shù)可以傳入其他的收集器,以便可以達到手機子組數(shù)據(jù)的目的。比如我們可以計算每種菜單分類的個數(shù),傳入一個counting

/**
 * 多級分組收集數(shù)據(jù)
 */
@Test
public void test03(){
    /** 計算每一種品類的菜單個數(shù) */
    Map<Dish.Type, Long> typeLongMap = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
    System.out.println(typeLongMap);
}

5. 按照謂詞分區(qū)

partitioningBy:通過傳遞一個條件只有true和false的結(jié)果的表達式,返回的結(jié)果中包括true(滿足條件)的集合以及false(不滿足條件)的集合。
比如,篩選出來質(zhì)數(shù)以及非質(zhì)數(shù),那么我們可以傳遞一個表達式或者一個方法,返回的是true和false,true表示是質(zhì)數(shù),false表示非質(zhì)數(shù)的集合。和按子組收集數(shù)據(jù)的區(qū)別就是,這里還可以收集到不滿足條件的所有元素集合。

/**
 * 將數(shù)字按質(zhì)數(shù)以及非質(zhì)數(shù)分區(qū)
 */
@Test
public void test08(){
    int demoInt = 100;
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, demoInt).boxed()
            .collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
    System.out.println(collect);
}

public boolean isPrime(int candidate){
    /** 通過傳遞的數(shù)字進行開方,我們只需要對傳遞的數(shù)字與開方的數(shù)字進行比對即可,計算次數(shù)會減少 */
    int candidateRoot = (int) Math.sqrt((double) candidate);
    /** 產(chǎn)生一個從2開始到開方跟的數(shù)字的數(shù)據(jù)流,與該數(shù)據(jù)流的每一個元素進行求余 */
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);// 表示沒有一個元素與開方根的數(shù)字求余等于0的
}

五、Collect靜態(tài)工廠方法表

工廠方法 返回類型 用途 示例
toList List<T> 把流中所有的項目收集到List dishList.collect(Collectors.toList())
toSet Set<T> 把流中所有的項目收集到Set dishList.collect(Collectors.toSet())
toCollection Collection<T> 把流中所有項目收集到給定的供應(yīng)源創(chuàng)建的集合 dishList.collect(Collectors. toCollection(), ArrayList::new)
counting Long 計算出來流中元素的個數(shù) dishList.collect(Collectors.counting)
summingInt Integer 計算出來集合中元素的某個屬性的和 int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
averagingInt Double 計算出集合中元素某個屬性的均值 Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
summarizingInt IntSummaryStatistics 計算出集合中元素某個屬性的統(tǒng)計值,包括最值、均值、總和等 IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
joinging String 連接流中每個元素調(diào)用toString進行拼接,使用傳遞的分隔符進行分割 String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
maxBy Optional<T> 通過傳遞的比較器收集元素中屬性最大的值,如果流為空則返回Optional.empty() Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
minBy Optional<T> 通過傳遞的比較器收集元素中屬性最小的值,如果流為空則返回Optional.empty()
reducing 歸約操作產(chǎn)生的類型 從一個作為累加器的起始值開始,利用BinaryOperator與流中的元素逐個結(jié)合,從而將流規(guī)約為單個值 Integer integer1 = dishList.stream().collect(Collectors.reducing(0,Dish::getColories,Integer::sum));
collectingAndThen 轉(zhuǎn)換函數(shù)返回的類型 包裹另外一個收集器,對其結(jié)果進行轉(zhuǎn)換
groupingBy Map<K, List<T>> 對流中元素的每個值進行分組
partitioningBy Map<boolean, List<T>> 對流中元素的每個值進行分區(qū)

六、開發(fā)自定義收集器

方式一

如果我們需要開發(fā)自己的自定義收集器的時候,需要讓我們自己的收集器去實現(xiàn)Collector接口。
Collector接口一共有五個方法去自己實現(xiàn),現(xiàn)在我們用開發(fā)我們自己的ToList收集器為例,寫一個我們自己的收集器。

package cn.liweidan.custom.collector;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * <p>Desciption:自定義ToList收集器</p>
 * CreateTime : 2017/7/10 下午6:37
 * Author : Weidan
 * Version : V1.0
 */
public class MyListCollector<T> implements Collector<T, List<T>, List<T>> {

    /*
        第一個泛型指的是需要收集的流的泛型
        第二個泛型指的是累加器在收集時候的類型
        第三個泛型指的是返回的類型(可能不是集合,比如counting())
     */

    /**
     * 建立一個新的結(jié)果容器
     * @return
     */
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    /**
     * 將元素累加到容器中去
     * @return
     */
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item);
    }

    /**
     * 對結(jié)果容器進行最終轉(zhuǎn)換(如需要轉(zhuǎn)換成Long返回,則在這一步體現(xiàn))
     * @return
     */
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();// 此處無需進行轉(zhuǎn)換,直接返回此函數(shù)即可
    }

    /**
     * 對每個子流中的數(shù)據(jù)進行規(guī)約操作
     * 即在集合流中,處理器會將集合流進行不停地分割,分割到一定的很多的小子流的時候,再進行操作
     * 在這一步就是將每一個小流中的元素合并到一起
     * @return
     */
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) ->{
            list1.addAll(list2);
            return list1;
        };
    }

    /**
     * 這個方法是定義流返回的情況,一共有三種情況,存放于Characteristics枚舉中
     * UNORDERED:規(guī)約結(jié)果不受項目的遍歷和累計順序的影響
     * CONCURRENT:accumulator函數(shù)可以從多個線程去調(diào)用。如果收集器沒有標記UNORDERED那他僅用在無需數(shù)據(jù)源才可以規(guī)約
     * INDENTITY_FINISH:表明完成器方法返回的是一個恒等函數(shù),可以跳過。標記這種情況則表示累加器A可以不加檢查的轉(zhuǎn)換為累加器B
     * @return
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.IDENTITY_FINISH));
    }
}

現(xiàn)在對我們自己的收集器進行測試,這里與自帶的收集器的區(qū)別就是我沒有定義工廠模式去拿到toList收集器的實例,而是需要自己手動new出來。

@Test
public void test(){
    List<Dish> collect = dishList.stream().collect(new MyListCollector<Dish>());
    System.out.println(collect);
}

方式二

方式二比較簡單,但是功能也稍微差一點。就是通過使用collect方法的重載方法進行自定義收集器,并不需要去實現(xiàn)Collector接口。

/**
 * 使用方式二進行自定義收集
 */
@Test
public void test02(){
    ArrayList<Object> collect = dishList.stream().collect(
            ArrayList::new, // 相當(dāng)于方式一的supplier()方法,用于創(chuàng)建一個容器
            List::add,// 相當(dāng)于方式一的accumulator方法,用于迭代遍歷每個元素進行加入容器
            List::addAll// 規(guī)約并行中所有的容器
    );
    System.out.println(collect);
}

另外值得注意的是,這個方法并不能傳遞任何關(guān)于characteristics的信息,也就是說,默認已經(jīng)給我們設(shè)定為INDENTITY_FINISH以及CONCURRENT了。

七、開發(fā)自己的質(zhì)數(shù)收集器

在前面我們已經(jīng)試驗過一個質(zhì)數(shù)收集器了,在這里使用自定義收集器再收集一次一定范圍內(nèi)的質(zhì)數(shù)。在之前,我們是使用小于被測數(shù)的平方根的數(shù)字進行對比,到了這里我們再做進一步的優(yōu)化,就是只拿小于被測數(shù)的平方根的質(zhì)數(shù)作為除數(shù)。

PrimeNumberCollector:

package cn.liweidan.custom.collector2;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;

/**
 * <p>Desciption:質(zhì)數(shù)收集器</p>
 * CreateTime : 2017/7/11 上午10:43
 * Author : Weidan
 * Version : V1.0
 */
public class PrimeNumberCollector implements Collector<Integer,
                                            Map<Boolean, List<Integer>>,
                                        Map<Boolean, List<Integer>>> {

    public static <A> List<A> takeWhile(List<A> list, Predicate<A> p){
        int i = 0;
        for (A a : list) {
            if(!p.test(a)){
                return list.subList(0, i);
            }
            i++;
        }
        return list;
    }

    /**
     * 拿到所有的質(zhì)數(shù),以及被測數(shù)字。取出小于被測數(shù)的平方根與所有質(zhì)數(shù)比較,只拿被測數(shù)與小于平方根的質(zhì)數(shù)做計算
     * @param primes
     * @param candidate
     * @return
     */
    public static boolean isPrime(List<Integer> primes, int candidate) {
        int candidateRoot = (int) Math.sqrt((double) candidate);
        return takeWhile(primes, i -> i <= candidateRoot)
                .stream()
                .noneMatch(p -> candidate % p == 0);
    }

    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
        return () -> new HashMap<Boolean, List<Integer>>(){{
            put(true, new ArrayList<>());
            put(false, new ArrayList<>());
        }};
    }

    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
            acc.get(isPrime(acc.get(true), candidate))
                    .add(candidate);
        };
    }

    @Override
    public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
        return null;
    }

    @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(Characteristics.IDENTITY_FINISH));
    }
}

測試:

@Test
public void test01(){
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, 100).boxed().collect(new PrimeNumberCollector());
    System.out.println(collect);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,969評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,432評論 25 708
  • 昨天寫文章寫著寫著睡著了,看來,真是老了,咳咳,誰來攙我一把,老身走不動了……怎么還沒人來摻我,算了,我還是自己走...
    飛雪_飄渺閱讀 332評論 4 3
  • 昨晚夢到一個漂亮的不像話的男孩子,好像,是我的兒子。 很高興,又在夢里有疑惑:是我的兒子嗎? 今早想起來,依然高興...
    顧嶠閱讀 101評論 0 0
  • 天高云淡鄉(xiāng)間好,蔬果俱鮮。 圍坐爐邊,約會尋常縷縷煙。 風(fēng)輕日暖深春意,眾鳥和弦。 細水流年,人在閑中便是仙。
    悠游魚閱讀 307評論 0 1