之前寫了JDK8集合流的入門以及篩選,也就是集合流使用的打開和中間操作。這次帶來的是不同的收集數(shù)據(jù)的方式。
本節(jié)代碼GitHub地址:https://github.com/WeidanLi/Java-jdk8-collect
一、準(zhǔn)備:
還是老規(guī)矩杜跷,使用菜單進(jìn)行示例。(代碼的話建議拷貝這部分)
二矫夷、收集器簡(jiǎn)介
收集器即收集東西的容器葛闷,它用于使用集合流的時(shí)候的終端操作,即我們?cè)谌粘5臉I(yè)務(wù)邏輯中把流進(jìn)行過濾也好口四,進(jìn)行篩選也好孵运,然后我們總該要有一個(gè)容器可以存放這些過濾后的元素。這時(shí)候的收集器就派上用場(chǎng)了蔓彩。如代碼所示一個(gè)最簡(jiǎn)單的收集器的使用實(shí)例(當(dāng)然我感覺平時(shí)應(yīng)該沒人這么無聊)
/**
* 收集器是可以收集一個(gè)流中的數(shù)據(jù)的一個(gè)容器
* 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. 計(jì)算元素的個(gè)數(shù)counting()
counting()用于總結(jié)流中元素的個(gè)數(shù),有兩種寫法旷赖,分別如代碼所示顺又。counting()使用起來還是比較簡(jiǎn)單的。
/**
* cn.liweidan.collect.Demo01#demo02()
* counting()使用
*/
@Test
public void demo02(){
/* 查詢卡路里大于400的菜單的個(gè)數(shù) */
Long count = dishList.stream().filter(dish -> dish.getColories() > 400).collect(Collectors.counting());
System.out.println("卡路里大于400的菜單個(gè)數(shù):" + count);
/* 第二種寫法 */
count = dishList.stream().filter(dish -> dish.getColories() > 400).count();
System.out.println("卡路里大于400的菜單個(gè)數(shù):" + count);
}
2.查找流中的元素某個(gè)屬性的最大值或者最小值
我們通常需要去拿到一個(gè)對(duì)象集合中對(duì)象某個(gè)屬性進(jìn)行查詢最大和最小值等孵,按照J(rèn)DK8以前的寫法是需要先去遍歷集合中所有的對(duì)象稚照,去讀取某個(gè)屬性的值,然后去記錄最大值或者最小值進(jìn)行記錄俯萌,全部遍歷完成以后把該值進(jìn)行返回果录。這種寫法,描述起來也麻煩咐熙,寫起來也麻煩弱恒。
JDK8的流中就可以比較方便的拿到上面的需求。只需要在流中定義需要什么東西棋恼,當(dāng)流完成以后就可以取到所需要的值了返弹。不過我們需要先定義一個(gè)比較器,來告訴JVM我們需要個(gè)什么值爪飘。
/**
* cn.liweidan.collect.Demo01#demo03()
* 取出最大值以及最小值
*/
@Test
public void demo03(){
/* 定義一個(gè)卡路里比較器 */
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());
}
不過有時(shí)候我們需要最大值以及最小值在一個(gè)流中取出义起,這時(shí)候我們可以使用后面的分組進(jìn)行實(shí)現(xiàn)。
4. 匯總
匯總即對(duì)集合中元素的某個(gè)屬性進(jìn)行統(tǒng)計(jì)师崎,如菜單中的所有卡路里的匯總默终。
/**
* cn.liweidan.collect.Demo01#demo04()
* 匯總:對(duì)集合中所有菜單的卡路里進(jìn)行統(tǒng)計(jì)計(jì)算
*/
@Test
public void demo04(){
int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
System.out.println(collect);
}
示例中我們使用了summingInt方法,當(dāng)然Collectors還提供了Long和Double方法對(duì)Long和Double進(jìn)行統(tǒng)計(jì)犁罩。
匯總不僅僅包括sum穷蛹,還包括了平均數(shù)、最大值最小值等等昼汗。Collectors同時(shí)還定義了averagingInt以及IntSummaryStatistics來分別拿出元素屬性的均值和所有統(tǒng)計(jì)數(shù)據(jù)(包括最大值、最小值鬼雀、均值等)
/**
* 查詢菜單集合中卡路里的平均值以及所有統(tǒng)計(jì)數(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方法可以把自動(dòng)調(diào)用對(duì)象的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. 實(shí)現(xiàn)自定義歸約--reduce使用
像蚊帳之前講的均值、最值這些操作励烦,其實(shí)都是官方對(duì)reduce常用方法的封裝谓着,如果官方提供的這些方法不能夠滿足要求的話,那么就需要我們自己來自定義reduce的實(shí)現(xiàn)了坛掠。
reduce需要傳入三個(gè)參數(shù):
第一個(gè)參數(shù)是規(guī)約操作的起始值赊锚,即如果要統(tǒng)計(jì)總值的時(shí)候治筒,那么起始值是0
第二個(gè)參數(shù)就是要調(diào)用的對(duì)象的方法了,即菜單的卡路里值
第三個(gè)參數(shù)就是一個(gè)BinaryOperator操作了舷蒲,在這里定義我們拿到的值的操作方式耸袜。即相加。
也可以直接只傳需要的操作牲平,去除前兩個(gè)參數(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());
/**
* 計(jì)算菜單總卡路里值
*/
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);
}
在計(jì)算總和的時(shí)候,推薦使用mapToInt纵柿,因?yàn)榭梢悦馊プ詣?dòng)裝箱拆箱的性能消耗蜈抓。
四、分組
1. 簡(jiǎn)單分組
我們經(jīng)常需要對(duì)數(shù)據(jù)進(jìn)行分組昂儒,特別是在數(shù)據(jù)庫(kù)操作的時(shí)候沟使。當(dāng)我們需要從一個(gè)集合中進(jìn)行分組,代碼會(huì)變得十分復(fù)雜荆忍,分組功能剛好能夠解決這個(gè)問題格带。我們可以對(duì)菜單中的類型進(jìn)行分組,也可以根據(jù)卡路里的大小對(duì)菜單進(jìn)行自定義的分組刹枉。
/**
* 簡(jiǎn)單分組
*/
@Test
public void test01(){
/** 按照屬性類型進(jìn)行分組 */
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)]}
/** 自定義簡(jiǎn)單的分組方式 */
Map<CaloricLevel, List<Dish>> map = dishList.stream().collect(Collectors.groupingBy(d -> {
/** 此處寫if的時(shí)候注意要顧及到所有的情況 */
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ìn)行多級(jí)分組叽唱,比如根據(jù)菜單的類型分組的情況下又要根據(jù)卡路里大小進(jìn)行分組。那么我們可以在groupingBy中再傳入第二個(gè)groupingBy微宝。
/**
* 多級(jí)分組
*/
@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é)的第二個(gè)參數(shù)傳遞的是一個(gè)groupingBy棺亭,但是收集器的第二個(gè)參數(shù)可以傳入其他的收集器,以便可以達(dá)到手機(jī)子組數(shù)據(jù)的目的蟋软。比如我們可以計(jì)算每種菜單分類的個(gè)數(shù)镶摘,傳入一個(gè)counting
/**
* 多級(jí)分組收集數(shù)據(jù)
*/
@Test
public void test03(){
/** 計(jì)算每一種品類的菜單個(gè)數(shù) */
Map<Dish.Type, Long> typeLongMap = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
System.out.println(typeLongMap);
}
5. 按照謂詞分區(qū)
partitioningBy:通過傳遞一個(gè)條件只有true和false的結(jié)果的表達(dá)式,返回的結(jié)果中包括true(滿足條件)的集合以及false(不滿足條件)的集合岳守。
比如凄敢,篩選出來質(zhì)數(shù)以及非質(zhì)數(shù),那么我們可以傳遞一個(gè)表達(dá)式或者一個(gè)方法湿痢,返回的是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ù)字進(jìn)行開方臀规,我們只需要對(duì)傳遞的數(shù)字與開方的數(shù)字進(jìn)行比對(duì)即可滩援,計(jì)算次數(shù)會(huì)減少 */
int candidateRoot = (int) Math.sqrt((double) candidate);
/** 產(chǎn)生一個(gè)從2開始到開方跟的數(shù)字的數(shù)據(jù)流,與該數(shù)據(jù)流的每一個(gè)元素進(jìn)行求余 */
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);// 表示沒有一個(gè)元素與開方根的數(shù)字求余等于0的
}
五塔嬉、Collect靜態(tài)工廠方法表
工廠方法 | 返回類型 | 用途 | 示例 |
---|---|---|---|
toList | List<T> | 把流中所有的項(xiàng)目收集到List | dishList.collect(Collectors.toList()) |
toSet | Set<T> | 把流中所有的項(xiàng)目收集到Set | dishList.collect(Collectors.toSet()) |
toCollection | Collection<T> | 把流中所有項(xiàng)目收集到給定的供應(yīng)源創(chuàng)建的集合 | dishList.collect(Collectors. toCollection(), ArrayList::new) |
counting | Long | 計(jì)算出來流中元素的個(gè)數(shù) | dishList.collect(Collectors.counting) |
summingInt | Integer | 計(jì)算出來集合中元素的某個(gè)屬性的和 | int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories)); |
averagingInt | Double | 計(jì)算出集合中元素某個(gè)屬性的均值 | Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories)); |
summarizingInt | IntSummaryStatistics | 計(jì)算出集合中元素某個(gè)屬性的統(tǒng)計(jì)值玩徊,包括最值租悄、均值、總和等 | IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories)); |
joinging | String | 連接流中每個(gè)元素調(diào)用toString進(jìn)行拼接佣赖,使用傳遞的分隔符進(jìn)行分割 | 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)生的類型 | 從一個(gè)作為累加器的起始值開始憎蛤,利用BinaryOperator與流中的元素逐個(gè)結(jié)合外傅,從而將流規(guī)約為單個(gè)值 | Integer integer1 = dishList.stream().collect(Collectors.reducing(0,Dish::getColories,Integer::sum)); |
collectingAndThen | 轉(zhuǎn)換函數(shù)返回的類型 | 包裹另外一個(gè)收集器,對(duì)其結(jié)果進(jìn)行轉(zhuǎn)換 | |
groupingBy | Map<K, List<T>> | 對(duì)流中元素的每個(gè)值進(jìn)行分組 | 略 |
partitioningBy | Map<boolean, List<T>> | 對(duì)流中元素的每個(gè)值進(jìn)行分區(qū) | 略 |
六俩檬、開發(fā)自定義收集器
方式一
如果我們需要開發(fā)自己的自定義收集器的時(shí)候萎胰,需要讓我們自己的收集器去實(shí)現(xiàn)Collector接口。
Collector接口一共有五個(gè)方法去自己實(shí)現(xiàn)棚辽,現(xiàn)在我們用開發(fā)我們自己的ToList收集器為例技竟,寫一個(gè)我們自己的收集器。
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>> {
/*
第一個(gè)泛型指的是需要收集的流的泛型
第二個(gè)泛型指的是累加器在收集時(shí)候的類型
第三個(gè)泛型指的是返回的類型(可能不是集合屈藐,比如counting())
*/
/**
* 建立一個(gè)新的結(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);
}
/**
* 對(duì)結(jié)果容器進(jìn)行最終轉(zhuǎn)換(如需要轉(zhuǎn)換成Long返回榔组,則在這一步體現(xiàn))
* @return
*/
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();// 此處無需進(jìn)行轉(zhuǎn)換,直接返回此函數(shù)即可
}
/**
* 對(duì)每個(gè)子流中的數(shù)據(jù)進(jìn)行規(guī)約操作
* 即在集合流中联逻,處理器會(huì)將集合流進(jìn)行不停地分割搓扯,分割到一定的很多的小子流的時(shí)候,再進(jìn)行操作
* 在這一步就是將每一個(gè)小流中的元素合并到一起
* @return
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) ->{
list1.addAll(list2);
return list1;
};
}
/**
* 這個(gè)方法是定義流返回的情況包归,一共有三種情況锨推,存放于Characteristics枚舉中
* UNORDERED:規(guī)約結(jié)果不受項(xiàng)目的遍歷和累計(jì)順序的影響
* CONCURRENT:accumulator函數(shù)可以從多個(gè)線程去調(diào)用。如果收集器沒有標(biāo)記UNORDERED那他僅用在無需數(shù)據(jù)源才可以規(guī)約
* INDENTITY_FINISH:表明完成器方法返回的是一個(gè)恒等函數(shù)公壤,可以跳過换可。標(biāo)記這種情況則表示累加器A可以不加檢查的轉(zhuǎn)換為累加器B
* @return
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.IDENTITY_FINISH));
}
}
現(xiàn)在對(duì)我們自己的收集器進(jìn)行測(cè)試,這里與自帶的收集器的區(qū)別就是我沒有定義工廠模式去拿到toList收集器的實(shí)例厦幅,而是需要自己手動(dòng)new出來沾鳄。
@Test
public void test(){
List<Dish> collect = dishList.stream().collect(new MyListCollector<Dish>());
System.out.println(collect);
}
方式二
方式二比較簡(jiǎn)單,但是功能也稍微差一點(diǎn)确憨。就是通過使用collect方法的重載方法進(jìn)行自定義收集器洞渔,并不需要去實(shí)現(xiàn)Collector接口。
/**
* 使用方式二進(jìn)行自定義收集
*/
@Test
public void test02(){
ArrayList<Object> collect = dishList.stream().collect(
ArrayList::new, // 相當(dāng)于方式一的supplier()方法缚态,用于創(chuàng)建一個(gè)容器
List::add,// 相當(dāng)于方式一的accumulator方法,用于迭代遍歷每個(gè)元素進(jìn)行加入容器
List::addAll// 規(guī)約并行中所有的容器
);
System.out.println(collect);
}
另外值得注意的是堤瘤,這個(gè)方法并不能傳遞任何關(guān)于characteristics的信息玫芦,也就是說,默認(rèn)已經(jīng)給我們?cè)O(shè)定為INDENTITY_FINISH以及CONCURRENT了本辐。
七桥帆、開發(fā)自己的質(zhì)數(shù)收集器
在前面我們已經(jīng)試驗(yàn)過一個(gè)質(zhì)數(shù)收集器了医增,在這里使用自定義收集器再收集一次一定范圍內(nèi)的質(zhì)數(shù)。在之前老虫,我們是使用小于被測(cè)數(shù)的平方根的數(shù)字進(jìn)行對(duì)比叶骨,到了這里我們?cè)僮鲞M(jìn)一步的優(yōu)化,就是只拿小于被測(cè)數(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ù)忽刽,以及被測(cè)數(shù)字。取出小于被測(cè)數(shù)的平方根與所有質(zhì)數(shù)比較夺欲,只拿被測(cè)數(shù)與小于平方根的質(zhì)數(shù)做計(jì)算
* @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));
}
}
測(cè)試:
@Test
public void test01(){
Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, 100).boxed().collect(new PrimeNumberCollector());
System.out.println(collect);
}