之前寫了JDK8集合流的入門以及篩選,也就是集合流使用的打開和中間操作。這次帶來的是不同的收集數據的方式。
本節代碼GitHub地址:https://github.com/WeidanLi/Java-jdk8-collect
一、準備:
還是老規矩,使用菜單進行示例。(代碼的話建議拷貝這部分)
二、收集器簡介
收集器即收集東西的容器,它用于使用集合流的時候的終端操作,即我們在日常的業務邏輯中把流進行過濾也好,進行篩選也好,然后我們總該要有一個容器可以存放這些過濾后的元素。這時候的收集器就派上用場了。如代碼所示一個最簡單的收集器的使用實例(當然我感覺平時應該沒人這么無聊)
/**
* 收集器是可以收集一個流中的數據的一個容器
* cn.liweidan.collect.Demo01#demo01
*/
@Test
public void demo01(){
List<Dish> collect = dishList.stream().collect(Collectors.toList());
System.out.println(collect);
}
三、JDK8提供的預定義收集器
官方為我們提供了預定義的收集器的一些常用的必要功能,分別有:
- 元素規約與匯總
- 元素分組
- 元素分區
1. 計算元素的個數counting()
counting()用于總結流中元素的個數,有兩種寫法,分別如代碼所示。counting()使用起來還是比較簡單的。
/**
* cn.liweidan.collect.Demo01#demo02()
* counting()使用
*/
@Test
public void demo02(){
/* 查詢卡路里大于400的菜單的個數 */
Long count = dishList.stream().filter(dish -> dish.getColories() > 400).collect(Collectors.counting());
System.out.println("卡路里大于400的菜單個數:" + count);
/* 第二種寫法 */
count = dishList.stream().filter(dish -> dish.getColories() > 400).count();
System.out.println("卡路里大于400的菜單個數:" + count);
}
2.查找流中的元素某個屬性的最大值或者最小值
我們通常需要去拿到一個對象集合中對象某個屬性進行查詢最大和最小值,按照JDK8以前的寫法是需要先去遍歷集合中所有的對象,去讀取某個屬性的值,然后去記錄最大值或者最小值進行記錄,全部遍歷完成以后把該值進行返回。這種寫法,描述起來也麻煩,寫起來也麻煩。
JDK8的流中就可以比較方便的拿到上面的需求。只需要在流中定義需要什么東西,當流完成以后就可以取到所需要的值了。不過我們需要先定義一個比較器,來告訴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());
}
不過有時候我們需要最大值以及最小值在一個流中取出,這時候我們可以使用后面的分組進行實現。
4. 匯總
匯總即對集合中元素的某個屬性進行統計,如菜單中的所有卡路里的匯總。
/**
* cn.liweidan.collect.Demo01#demo04()
* 匯總:對集合中所有菜單的卡路里進行統計計算
*/
@Test
public void demo04(){
int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
System.out.println(collect);
}
示例中我們使用了summingInt方法,當然Collectors還提供了Long和Double方法對Long和Double進行統計。
匯總不僅僅包括sum,還包括了平均數、最大值最小值等等。Collectors同時還定義了averagingInt以及IntSummaryStatistics來分別拿出元素屬性的均值和所有統計數據(包括最大值、最小值、均值等)
/**
* 查詢菜單集合中卡路里的平均值以及所有統計數據
*/
@Test
public void demo05(){
/* 查詢所有菜單卡路里的平均值 */
Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
System.out.println("卡路里均值:" + collect);
/* 查詢菜單中所有的匯總數據 */
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方法可以把自動調用對象的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. 實現自定義歸約--reduce使用
像蚊帳之前講的均值、最值這些操作,其實都是官方對reduce常用方法的封裝,如果官方提供的這些方法不能夠滿足要求的話,那么就需要我們自己來自定義reduce的實現了。
reduce需要傳入三個參數:
第一個參數是規約操作的起始值,即如果要統計總值的時候,那么起始值是0
第二個參數就是要調用的對象的方法了,即菜單的卡路里值
第三個參數就是一個BinaryOperator操作了,在這里定義我們拿到的值的操作方式。即相加。
也可以直接只傳需要的操作,去除前兩個參數。
/**
* 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,// 轉換函數
Integer::sum));// 累積函數
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. 簡單分組
我們經常需要對數據進行分組,特別是在數據庫操作的時候。當我們需要從一個集合中進行分組,代碼會變得十分復雜,分組功能剛好能夠解決這個問題。我們可以對菜單中的類型進行分組,也可以根據卡路里的大小對菜單進行自定義的分組。
/**
* 簡單分組
*/
@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. 多級分組
如果我們需要進行多級分組,比如根據菜單的類型分組的情況下又要根據卡路里大小進行分組。那么我們可以在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. 按子組收集數據
在上一節的第二個參數傳遞的是一個groupingBy,但是收集器的第二個參數可以傳入其他的收集器,以便可以達到手機子組數據的目的。比如我們可以計算每種菜單分類的個數,傳入一個counting
/**
* 多級分組收集數據
*/
@Test
public void test03(){
/** 計算每一種品類的菜單個數 */
Map<Dish.Type, Long> typeLongMap = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
System.out.println(typeLongMap);
}
5. 按照謂詞分區
partitioningBy:通過傳遞一個條件只有true和false的結果的表達式,返回的結果中包括true(滿足條件)的集合以及false(不滿足條件)的集合。
比如,篩選出來質數以及非質數,那么我們可以傳遞一個表達式或者一個方法,返回的是true和false,true表示是質數,false表示非質數的集合。和按子組收集數據的區別就是,這里還可以收集到不滿足條件的所有元素集合。
/**
* 將數字按質數以及非質數分區
*/
@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){
/** 通過傳遞的數字進行開方,我們只需要對傳遞的數字與開方的數字進行比對即可,計算次數會減少 */
int candidateRoot = (int) Math.sqrt((double) candidate);
/** 產生一個從2開始到開方跟的數字的數據流,與該數據流的每一個元素進行求余 */
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);// 表示沒有一個元素與開方根的數字求余等于0的
}
五、Collect靜態工廠方法表
工廠方法 | 返回類型 | 用途 | 示例 |
---|---|---|---|
toList | List<T> | 把流中所有的項目收集到List | dishList.collect(Collectors.toList()) |
toSet | Set<T> | 把流中所有的項目收集到Set | dishList.collect(Collectors.toSet()) |
toCollection | Collection<T> | 把流中所有項目收集到給定的供應源創建的集合 | dishList.collect(Collectors. toCollection(), ArrayList::new) |
counting | Long | 計算出來流中元素的個數 | 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 | 計算出集合中元素某個屬性的統計值,包括最值、均值、總和等 | IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories)); |
joinging | String | 連接流中每個元素調用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 | 歸約操作產生的類型 | 從一個作為累加器的起始值開始,利用BinaryOperator與流中的元素逐個結合,從而將流規約為單個值 | Integer integer1 = dishList.stream().collect(Collectors.reducing(0,Dish::getColories,Integer::sum)); |
collectingAndThen | 轉換函數返回的類型 | 包裹另外一個收集器,對其結果進行轉換 | |
groupingBy | Map<K, List<T>> | 對流中元素的每個值進行分組 | 略 |
partitioningBy | Map<boolean, List<T>> | 對流中元素的每個值進行分區 | 略 |
六、開發自定義收集器
方式一
如果我們需要開發自己的自定義收集器的時候,需要讓我們自己的收集器去實現Collector接口。
Collector接口一共有五個方法去自己實現,現在我們用開發我們自己的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())
*/
/**
* 建立一個新的結果容器
* @return
*/
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
/**
* 將元素累加到容器中去
* @return
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
/**
* 對結果容器進行最終轉換(如需要轉換成Long返回,則在這一步體現)
* @return
*/
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();// 此處無需進行轉換,直接返回此函數即可
}
/**
* 對每個子流中的數據進行規約操作
* 即在集合流中,處理器會將集合流進行不停地分割,分割到一定的很多的小子流的時候,再進行操作
* 在這一步就是將每一個小流中的元素合并到一起
* @return
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) ->{
list1.addAll(list2);
return list1;
};
}
/**
* 這個方法是定義流返回的情況,一共有三種情況,存放于Characteristics枚舉中
* UNORDERED:規約結果不受項目的遍歷和累計順序的影響
* CONCURRENT:accumulator函數可以從多個線程去調用。如果收集器沒有標記UNORDERED那他僅用在無需數據源才可以規約
* INDENTITY_FINISH:表明完成器方法返回的是一個恒等函數,可以跳過。標記這種情況則表示累加器A可以不加檢查的轉換為累加器B
* @return
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.IDENTITY_FINISH));
}
}
現在對我們自己的收集器進行測試,這里與自帶的收集器的區別就是我沒有定義工廠模式去拿到toList收集器的實例,而是需要自己手動new出來。
@Test
public void test(){
List<Dish> collect = dishList.stream().collect(new MyListCollector<Dish>());
System.out.println(collect);
}
方式二
方式二比較簡單,但是功能也稍微差一點。就是通過使用collect方法的重載方法進行自定義收集器,并不需要去實現Collector接口。
/**
* 使用方式二進行自定義收集
*/
@Test
public void test02(){
ArrayList<Object> collect = dishList.stream().collect(
ArrayList::new, // 相當于方式一的supplier()方法,用于創建一個容器
List::add,// 相當于方式一的accumulator方法,用于迭代遍歷每個元素進行加入容器
List::addAll// 規約并行中所有的容器
);
System.out.println(collect);
}
另外值得注意的是,這個方法并不能傳遞任何關于characteristics的信息,也就是說,默認已經給我們設定為INDENTITY_FINISH以及CONCURRENT了。
七、開發自己的質數收集器
在前面我們已經試驗過一個質數收集器了,在這里使用自定義收集器再收集一次一定范圍內的質數。在之前,我們是使用小于被測數的平方根的數字進行對比,到了這里我們再做進一步的優化,就是只拿小于被測數的平方根的質數作為除數。
PrimeNumberCollector:
package cn.liweidan.custom.collector2;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
/**
* <p>Desciption:質數收集器</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;
}
/**
* 拿到所有的質數,以及被測數字。取出小于被測數的平方根與所有質數比較,只拿被測數與小于平方根的質數做計算
* @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);
}