Java8(八)——Stream的collector操作

前言

本篇博客對應《Java8 in action》一書的第六章,主要總結收集器的操作,在第六章的開頭,這本中用一個簡單的例子介紹了使用收集器的便捷。這裏還是先總結一下這個實例。在原書實例的基礎上,我們做了一個改良

需求:對一些交易數據,按照交易年限進行分組

這一些交易數據如下:

@Slf4j
public class TransactionContainer {

    public static List<Trader> getTraderList(){
        List<Trader> traderList = new ArrayList<>();
        //交易員
        Trader raoul  = new Trader("raoul","Cambridge");
        Trader mario  = new Trader("mario","Milan");
        Trader alan  = new Trader("alan","Cambridge");
        Trader brian  = new Trader("brian","Cambridge");

        traderList.add(raoul);
        traderList.add(mario);
        traderList.add(alan);
        traderList.add(brian);

        return traderList;
    }

    public static List<Transaction> getTransactionList(){
        //交易員
        Trader raoul  = new Trader("raoul","Cambridge");
        Trader mario  = new Trader("mario","Milan");
        Trader alan  = new Trader("alan","Cambridge");
        Trader brian  = new Trader("brian","Cambridge");
        //交易記錄
        List<Transaction> transactions = Arrays.asList(
                new Transaction(brian,2011,300),
                new Transaction(raoul,2012,1000),
                new Transaction(raoul,2011,400),
                new Transaction(mario,2012,710),
                new Transaction(mario,2012,700),
                new Transaction(alan,2012,950));
        return transactions;
    }
}

現在需要按照交易年限分組,分爲2011年的交易一組,2012年的交易一組,如果沒有收集器我們的操作會是這個樣子的

/**
 * 沒有使用collector
 * @param transactionList
 */
public static void outOfUseCollector(List<Transaction> transactionList){
    Map<Integer,List<Transaction>> transactionByYear = new HashMap<>();
    for(Transaction transaction:transactionList){
        int year = transaction.getYear();
        List<Transaction> transactionYear = transactionByYear.get(year);
        if(transactionYear == null){
            transactionYear = new ArrayList<>();
            transactionByYear.put(year,transactionYear);
        }
        transactionYear.add(transaction);
    }
    log.info("{}",transactionByYear);
}

繁瑣,非常之繁瑣。如果有了收集器,我們的操作會是這個樣子的

/**
 * 使用collector
 * @param transactionList
 */
public static void useCollector(List<Transaction> transactionList){
    Map<Integer, List<Transaction>> result = transactionList.stream().collect(groupingBy(Transaction::getYear));
    log.info("use collector result : {}",result);
}

還說啥?簡單,非常之簡單。

這本書中將流收集器的使用分爲了幾個方面——歸約和彙總,分組,分區。我們會一一進行總結

collect(),Collector,Collectors三者的區別

在正式學習這些收集器之前,我們需要區分 collect(),Collector,Collectors三者的區別。

collect是一個方法——java.util.Stream類的內部方法,主要用於將Stream中的數據歸約或者分組成另外一種形式。對流調用collect方法將對流中的元素觸發一個歸約操作(由Collector來參數化)

Collector是一個接口——定義了一些基本的歸約和分組操作,這個在後面會詳細介紹。一般來說,Collector會對元素應用一個轉換函數(很多時候是不體現任何效果的恆等轉換,例如toList),並將結果累積在一個數據結構中,從而產生這一過程的最終輸出

Collectors是一個工具類——不僅提供了對應的實現類還提供了各種快速生成Collector實例的工具方法。我們這片博客主要就是總結Collectors中的一些方法和操作。Collectors實用類提供了很多靜態工廠方法用於進行歸約操作

隨着整個博客的梳理,後續對這三者的區別會有一個更深的體會。

歸約和彙總

歸約和彙總指的是將流元素歸約和彙總爲一個值。這我們還是使用之前的菜單實例來進行操作。在需要將流項目重組成集合時,一般會使用收集器( Stream方法collect的參數)。再寬泛一點來說,但凡要把流中所有的項目合併成一個結果時就可以用。這個結果可以是任何類型,可以複雜如代表一棵樹的多級映,或是簡單如一個整數——也許代表了菜單的熱量總和

Dish實例

package com.learn.stream.common;

/**
 * autor:liman
 * createtime:2019/8/14
 * comment: 菜餚
 */
public class Dish {

    private final String name;//姓名
    private final boolean vegetarian;//是否是蔬菜
    private final int calories;//熱量
    private final Type type;//類型

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    public enum Type{MEAT,FISH,OTHER}

    @Override
    public String toString() {
        return "Dish{" +
                "name='" + name + '\'' +
                ", vegetarian=" + vegetarian +
                ", calories=" + calories +
                ", type=" + type +
                '}';
    }
}

菜單實例

public class DishContainer {

    private static List<Dish> dishList = new ArrayList<>();

    public static List<Dish> getDishList(){
        dishList = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH) );
        return dishList;
    }

}

統計個數

/**
 * 測試count的方法——直接統計菜單中有多少個菜
 */
public static void testCollectorCount(List<Dish> dishList) {
    Long dishCount = dishList.stream().collect(Collectors.counting());
    log.info("discount num : {}", dishCount);
    //也可以直接寫成
    //dishList.stream().count();
}

查找最大值和最小值

/**
 * 獲取流中的最大值和最小值
 *
 * @param dishList
 */
public static void testGetMaxAndMin(List<Dish> dishList) {
    //查詢菜單中calories最大的
    dishList.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)))
            .ifPresent(e -> log.info("max calory dish:{}", e));
	//查詢菜單中calories最小的
    dishList.stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)))
            .ifPresent(e -> log.info("min calory dish:{}", e));
}

彙總

Collectors專門爲彙總提供了一些方法

求和

/**
 * 統計所有的calorie
 *
 * @param dishList
 */
public static void testSummingInt(List<Dish> dishList) {
    Integer totalCalories = dishList.stream().collect(Collectors.summingInt(Dish::getCalories));
    log.info("all calories:{}", totalCalories);
}

求平均數

/**
 * 求平均熱量
 *
 * @param dishList
 */
public static void testAverageCalories(List<Dish> dishList) {
	Double averageCalories = dishList.stream().collect(Collectors.averagingInt(Dish::getCalories));
	log.info("average calories:{}", averageCalories);
}

到目前爲止,你已經看到了如何使用收集器來給流中的元素計數,找到這些元素數值屬性的最大值和最小值,以及計算其總和和平均值。不過很多時候,你可能想要得到兩個或更多這樣的結果,而且你希望只需一次操作就可以完成。在這種情況下,你可以使用summarizingInt工廠方法返回的收集器。

多種結果彙總

IntSummaryStatistics summaryStatistics = dishList.stream().collect(Collectors.summarizingInt(Dish::getCalories));
log.info("summary info : {}",summaryStatistics);

連接字符串

joining工廠方法返回的收集器會把對流中每一個對象應用toString方法得到的所有字符串連接成一個字符串

/**
 * @param dishList
 */
public static void testJoin(List<Dish> dishList) {
    String allDishName = dishList.stream().map(Dish::getName).collect(Collectors.joining());
    log.info("all dish name with no spilt : {}", allDishName);
	//joining方法可以接受元素之間的分界符
    String allDishNameWithSpilt = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
    log.info("all dish name with spilt:{}", allDishNameWithSpilt);
}

歸約和彙總的本質

前面總結的所有的歸約操作其實都是reducing操作的特殊情況而已。Collectors.reducing工廠方法是所有這些特殊情況的一般化

我們計算所有菜卡路里的總和還可以採用如下操作

Integer totalCalories = dishList.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));

可以直接利用reducing操作,可以看看reducing的源碼如下:

public static <T, U> Collector<T, ?, U> reducing(U identity,Function<? super T, ? extends U> mapper,BinaryOperator<U> op) {
    return new CollectorImpl<>(
            boxSupplier(identity),
            (a, t) -> { a[0] = op.apply(a[0], mapper.apply(t)); },
            (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
            a -> a[0], CH_NOID);
}

可以看到reducing需要三個參數

1、第一個參數是歸約操作的起始值,即初始值

2、第二個參數是一個Function<T,R>接口

3、第三個參數是BinaryOperator<T,T,T>接口,這個接口是將兩個參數進行彙總的操作

也可以通過以下操作找到熱量最高的菜品

Dish dish = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)).get()

分組

簡單分組

假設你要把菜單中的菜按照類型進行分類,有肉的放一組,有魚的放一組,其他的都放另一組。用Collectors.groupingBy工廠方法返回的收集器就可以輕鬆地完成這項任務,可以如下操作

public static void testSimpleGroupBy(List<Dish> dishList){
    Map<Dish.Type, List<Dish>> result = dishList.stream().collect(Collectors.groupingBy(Dish::getType));
    log.info("simple group by result : {}",result);
}

這裏,我們給groupingBy方法傳遞了一個Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我們把這個Function叫作分類函數,因爲它用來把流中的元素分成不同的組。

分組操作的結果是一個Map,把分組函數返回的值作爲映射的鍵,把流中所有具有這個分類值的項目的列表作爲對應的映射值。

多級分組

多級分組需要一個多參數的groupBy操作,在Collectors類中,提供了兩個多參數的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);
}

第二個參數接受Collector類型的參數,而groupBy恰好返回的也是一個Collector類型的參數,這就意味着我們可以直接將groupingBy作爲第二個參數傳遞給上一層的groupingBy方法

/**
 * 測試多級分組
 * @param dishList
 */
public static void testMultiLevelGroupBy(List<Dish> dishList){

	//定義底層分組的邏輯
    Function<Dish,CaloricLevel> deepGroupFunction = 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>>> deepGroupResult = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(deepGroupFunction)));

    log.info("多級分組的結果:{}",deepGroupResult);
}

運行結果示例如下:

{
FISH={NORMAL=[Dish{name='salmon', vegetarian=false, calories=450, type=FISH}], DIET=[Dish{name='prawns', vegetarian=false, calories=300, type=FISH}]}, 

MEAT={NORMAL=[Dish{name='beef', vegetarian=false, calories=700, type=MEAT}, Dish{name='chicken', vegetarian=false, calories=400, type=MEAT}], FAT=[Dish{name='pork', vegetarian=false, calories=800, type=MEAT}]}, 

OTHER={NORMAL=[Dish{name='french fries', vegetarian=true, calories=530, type=OTHER}, Dish{name='pizza', vegetarian=true, calories=550, type=OTHER}], DIET=[Dish{name='rice', vegetarian=true, calories=350, type=OTHER}, Dish{name='season fruit', vegetarian=true, calories=120, type=OTHER}]}
}

多級分組統計

傳遞給第一個groupingBy的第二個參數可以是任意的Collector接口,正常的統計類方法也可以,如下所示

public static void testGroupByCount(List<Dish> dishList){
    Map<Dish.Type, Long> groupByCountResult = dishList.stream().collect(Collectors.groupingBy(
            Dish::getType, Collectors.counting()
    ));
    log.info("group by count:{}",groupByCountResult);
}

運行結果

group by count:{FISH=2, MEAT=3, OTHER=4}

再舉一個例子

/**
 * 分組統計,找到每個類型中卡路里最高的蔬菜
 * @param dishList
 */
public static void testGroupByMax(List<Dish> dishList){
    Map<Dish.Type, Optional<Dish>> maxWithGroupBy = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
    log.info("max with group by result : {}",maxWithGroupBy);
}

運行結果

{
	FISH=Optional[Dish{
		name='salmon',
		vegetarian=false,
		calories=450,
		type=FISH
	}],
	MEAT=Optional[Dish{
		name='pork',
		vegetarian=false,
		calories=800,
		type=MEAT
	}],
	OTHER=Optional[Dish{
		name='pizza',
		vegetarian=true,
		calories=550,
		type=OTHER
	}]
}

分區

分區其實是分組的特殊化實例,只是分區接收的是一個返回boolean類型的函數,可以看看如下實例

List<Dish> dishList = DishContainer.getDishList();
Map<Boolean, List<Dish>> result = dishList.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
log.info("{}",result.get(true));

上述代碼中的partitioningBy接受的是一個返回爲boolean類型的函數。上述代碼運行結果

{
	false=[Dish{name='pork', vegetarian=false, calories=800, type=MEAT}], 
	true=[Dish{name='french fries', vegetarian=true, calories=530, type=OTHER}]
}

之後我們只需要利用result.get(true)就可以獲取到蔬菜了。

上述操作可以通過之前說的filter一樣能完成

List<Dish> filterResult = dishList.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

但是分區保留了true和false的關鍵key值

同樣分區也可以對多級,我們操作的一個單參數的partitioningBy函數,其實底層調用的也是接受一個Predicate和Collector參數的方法

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

我們上面操作的partitioningBy接口,其實底層也是調用的這個方法

public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
    return partitioningBy(predicate, toList());
}

因此我們可以partitioningBy進行多級分組

public static void testDeepPartition(){
    List<Dish> dishList = DishContainer.getDishList();
    Map<Boolean, Map<Dish.Type, List<Dish>>> result = dishList.stream().collect(Collectors.partitioningBy(Dish::isVegetarian, Collectors.groupingBy(Dish::getType)));
    log.info("{}",result);
}

Java8實戰一書中還介紹了將一堆數分爲質數和非質數的操作,具體如下

判斷是否是質數

/**
 * 判斷一個數是不是質數
 *
 * @param candidate
 * @return
 */
public static boolean isPrime(int candidate) {
    int candidateRoot = (int) Math.sqrt((double) candidate);
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);//如果一個都沒法整除,則是質數
}

將數據進行分組區分

/**
 * 將數據流分成質數和非質數
 */
public static void departPrimeAndNoPrime(int n) {
    Map<Boolean, List<Integer>> result = IntStream.range(2, n).boxed()
            .collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
    log.info("prime result : {}",result.get(true));
}

總結

本篇博客總結了Java8中stream中collect的各種操作,按照《Java8 in action》一書中最後的結尾還自定義了一個Collector的操作,這裏不再總結了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章