JAVA8 流 Stream 的使用

你可以把Java8的流看做花哨又懶惰的數(shù)據(jù)集迭代器。他們支持兩種類型的操作:中間操作(e.g. filter, map)和終端操作(如count, findFirst, forEach, reduce). 中間操作可以連接起來,將一個流轉(zhuǎn)換為另一個流。這些操作不會消耗流,其目的是建立一個流水線。與此相反,終端操作會消耗類,產(chǎn)生一個最終結果。

JAVA8流的使用

詳解Java8 Collect收集Stream的方法

collect就是一個歸約操作,就像reduce一樣可以接受各種做法作為參數(shù),將流中的元素累積成一個匯總結果。具體的做法是通過定義新的Collector接口來定義的。

案例:

最大值,最小值,平均值

// 為啥返回Optional? 如果stream為null怎么辦, 這時候Optinal就很有意義了

?Optional<Dish> mostCalorieDish = dishes.stream().max(Comparator.comparingInt(Dish::getCalories));?

Optional<Dish> minCalorieDish = dishes.stream().min(Comparator.comparingInt(Dish::getCalories));?

Double avgCalories = dishes.stream().collect(Collectors.averagingInt(Dish::getCalories));

?IntSummaryStatistics summaryStatistics = dishes.stream().collect(Collectors.summarizingInt(Dish::getCalories));?

double average = summaryStatistics.getAverage();?

long count = summaryStatistics.getCount();?

int max = summaryStatistics.getMax();?

int min = summaryStatistics.getMin();?

long sum = summaryStatistics.getSum();

連接收集器

String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining());? ??//直接連接

String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));? ?//逗號

list.stream().collect(Collectors.joings(",")) ;? //將List集合中的元素轉(zhuǎn)換為一個流,然后將這些元素連接成一個以逗號分隔的字符串

toList

?//將原來的Stream映射為一個單元素流,然后收集為List

List<String> names = dishes.stream().map(Dish::getName).collect(toList());?

toSet

//將Type收集為一個set,可以去重復

Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??

toMap

?//Dish::getType:表示使用?Dish?對象的?getType()?方法作為 Map 的鍵。? d -> d:表示將每個?Dish?對象本身作為 Map 的值

Map<Type, Dish> byType = dishes.stream().collect(toMap(Dish::getType, d -> d));? ? ? ? ? ? ? ? ??

有時候可能需要將一個數(shù)組轉(zhuǎn)為map,做緩存,方便多次計算獲取。toMap提供的方法k和v的生成函數(shù)。(注意,上述demo是一個坑,不可以這樣用!??!, 請使用toMap(Function, Function, BinaryOperator)) 。

toMap(Function, Function, BinaryOperator)?是?Collectors.toMap()?方法的一個重載版本,用于在收集元素到 Map 的過程中處理重復鍵的情況。讓我們來解釋這三個參數(shù)的含義:

第一個參數(shù)?Function:這個函數(shù)指定了如何從元素中提取鍵。在這個參數(shù)中,你可以傳入一個函數(shù),用于從元素中提取鍵值。在這個例子中,Dish::getType?表示使用?Dish?對象的?getType()?方法作為鍵。

第二個參數(shù)?Function:這個函數(shù)指定了如何從元素中提取值。在這個參數(shù)中,你可以傳入一個函數(shù),用于從元素中提取值。在這個例子中,d -> d?表示將每個?Dish?對象本身作為值。

第三個參數(shù)?BinaryOperator:這個函數(shù)定義了當出現(xiàn)重復鍵時如何處理這些值。如果 Map 中已經(jīng)存在相同的鍵,BinaryOperator?將被調(diào)用來決定如何合并現(xiàn)有值和新值。常見的操作包括覆蓋現(xiàn)有值、保留現(xiàn)有值或者自定義合并邏輯。

//Dish::getType?用于提取鍵;Function.identity()?用于提取值,這里表示保留原始值;(existing, replacement) -> replacement?是一個?BinaryOperator,它表示當出現(xiàn)重復鍵時,選擇新值覆蓋舊值。

Map<Type, Dish> byType = dishes.stream().collect(Collectors.toMap(Dish::getType, Function.identity(), (existing, replacement) -> replacement));

// (existing, replacement) -> existing?是一個?BinaryOperator,它表示當出現(xiàn)重復鍵時,選擇保留已經(jīng)存在的值而不覆蓋。

Map<Type, Dish> byType = dishes.stream() .collect(Collectors.toMap(Dish::getType, Function.identity(), (existing, replacement) -> existing));


自定義收集器

自定義歸約reducing

前面幾個都是reducing工廠方法定義的歸約過程的特殊情況,其實可以用Collectors.reducing創(chuàng)建收集器。比如,求和

// reducing收集器是一個可以用來累加元素的收集器,它接受三個參數(shù):初始值、轉(zhuǎn)換函數(shù)和累加器函數(shù)。

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

//使用內(nèi)置函數(shù)代替箭頭函數(shù)

Integer totalCalories2 = dishes.stream().collect(reducing(0, Dish::getCalories, Integer::sum));

// reducing除了接收一個初始值,還可以把第一項當作初始值

Optional<Dish> mostCalorieDish = dishes.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));


當然也可以直接使用reduce

// map(Dish::getCalories)將每個菜肴對象映射為其熱量值

Optional<Integer> totalCalories3 = dishes.stream().map(Dish::getCalories).reduce(Integer::sum);


雖然都可以,但考量效率的話,還是要選擇下面這種

intsum = dishes.stream().mapToInt(Dish::getCalories).sum();

reducing

關于reducing的用法比較復雜,目標在于把兩個值合并成一個值。

public static<T, U>

??Collector<T, ?, U> reducing(U identity,

????????????????Function<? superT, ? extendsU> mapper,

????????????????BinaryOperator<U> op)

首先看到3個泛型,

U是返回值的類型,比如上述demo中計算熱量的,U就是Integer。

關于T,T是Stream里的元素類型。由Function的函數(shù)可以知道,mapper的作用就是接收一個參數(shù)T,然后返回一個結果U。對應demo中Dish。

?在返回值Collector的泛型列表的中間,這個表示容器類型,一個收集器當然需要一個容器來存放數(shù)據(jù)。這里的?則表示容器類型不確定。事實上,在這里的容器就是U[]。

關于參數(shù):

identity是返回值類型的初始值,可以理解為累加器的起點。

mapper則是map的作用,意義在于將Stream流轉(zhuǎn)換成你想要的類型流。

op則是核心函數(shù),作用是如何處理兩個變量。其中,第一個變量是累積值,可以理解為sum,第二個變量則是下一個要計算的元素。從而實現(xiàn)了累加。

reducing還有一個重載的方法,可以省略第一個參數(shù),意義在于把Stream里的第一個參數(shù)當做初始值。

public static<T> Collector<T, ?, Optional<T>>

??reducing(BinaryOperator<T> op)

先看返回值的區(qū)別,T表示輸入值和返回值類型,即輸入值類型和輸出值類型相同。還有不同的就是Optional了。這是因為沒有初始值,而第一個參數(shù)有可能是null,當Stream的元素是null的時候,返回Optional就很意義了。

再看參數(shù)列表,只剩下BinaryOperator。BinaryOperator是一個三元組函數(shù)接口,目標是將兩個同類型參數(shù)做計算后返回同類型的值??梢园凑?>2? 1:2來理解,即求兩個數(shù)的最大值。求最大值是比較好理解的一種說法,你可以自定義lambda表達式來選擇返回值。那么,在這里,就是接收兩個Stream的元素類型T,返回T類型的返回值。用sum累加來理解也可以。

上述的demo中發(fā)現(xiàn)reduce和collect的作用幾乎一樣,都是返回一個最終的結果,比如,我們可以使用reduce實現(xiàn)toList效果:

//手動實現(xiàn)toListCollector --- 濫用reduce, 不可變的規(guī)約---不可以并行

List<Integer> calories = dishes.stream().map(Dish::getCalories)

????.reduce(newArrayList<Integer>(),

????????(List<Integer> l, Integer e) -> {

??????????l.add(e);

??????????returnl;

????????},

????????(List<Integer> l1, List<Integer> l2) -> {

??????????l1.addAll(l2);

??????????returnl1;

????????}

????);

關于上述做法解釋一下。

<U> U reduce(U identity,

?????????BiFunction<U, ? superT, U> accumulator,

?????????BinaryOperator<U> combiner);

U是返回值類型,這里就是List

BiFunction accumulator是是累加器,目標在于累加值和單個元素的計算規(guī)則。這里就是List和元素做運算,最終返回List。即,添加一個元素到list。

BinaryOperator combiner是組合器,目標在于把兩個返回值類型的變量合并成一個。這里就是兩個list合并。

這個解決方案有兩個問題:一個是語義問題,一個是實際問題。語義問題在于,reduce方法旨在把兩個值結合起來生成一個新值,它是一個不可變歸約。相反,collect方法的設計就是要改變?nèi)萜?,從而累積要輸出的結果。這意味著,上面的代碼片段是在濫用reduce方法,因為它在原地改變了作為累加器的List。錯誤的語義來使用reduce方法還會造成一個實際問題:這個歸約不能并行工作,因為由多個線程并發(fā)修改同一個數(shù)據(jù)結構可能會破壞List本身。在這種情況下,如果你想要線程安全,就需要每次分配一個新的List,而對象分配又會影響性能。這就是collect適合表達可變?nèi)萜魃系臍w約的原因,更關鍵的是它適合并行操作。

總結:reduce適合不可變?nèi)萜鳉w約,collect適合可變?nèi)萜鳉w約。collect適合并行

分組

數(shù)據(jù)庫中經(jīng)常遇到分組求和的需求,提供了group by原語。在Java里, 如果按照指令式風格(手動寫循環(huán))的方式,將會非常繁瑣,容易出錯。而Java8則提供了函數(shù)式解法。

比如,將dish按照type分組。和前面的toMap類似,但分組的value卻不是一個dish,而是一個List。

Map<Type, List<Dish>> dishesByType = dishes.stream().collect(groupingBy(Dish::getType));

這里

publicstatic<T, K> Collector<T, ?, Map<K, List<T>>>

??groupingBy(Function<? superT, ? extendsK> classifier)

參數(shù)分類器為Function,旨在接收一個參數(shù),轉(zhuǎn)換為另一個類型。上面的demo就是把stream的元素dish轉(zhuǎn)成類型Type,然后根據(jù)Type將stream分組。其內(nèi)部是通過HashMap來實現(xiàn)分組的。groupingBy(classifier, HashMap::new, downstream);

除了按照stream元素自身的屬性函數(shù)去分組,還可以自定義分組依據(jù),比如根據(jù)熱量范圍分組。

既然已經(jīng)知道groupingBy的參數(shù)為Function, 并且Function的參數(shù)類型為Dish,那么可以自定義分類器為:

private CaloricLevel getCaloricLevel(Dish d) {

??if(d.getCalories() <= 400) {

???returnCaloricLevel.DIET;

??} elseif(d.getCalories() <= 700) {

???returnCaloricLevel.NORMAL;

??} else{

???returnCaloricLevel.FAT;

??}

}

再傳入?yún)?shù)即可

Map<CaloricLevel, List<Dish>> dishesByLevel = dishes.stream().collect(groupingBy(this::getCaloricLevel));

多級分組

groupingBy還重載了其他幾個方法,比如

publicstatic<T, K, A, D>

??Collector<T, ?, Map<K, D>> groupingBy(Function<? superT, ? extendsK> classifier,

?????????????????????Collector<? superT, A, D> downstream)

泛型多的恐怖。簡單的認識一下。classifier還是分類器,就是接收stream的元素類型,返回一個你想要分組的依據(jù),也就是提供分組依據(jù)的基數(shù)的。所以T表示stream當前的元素類型,K表示分組依據(jù)的元素類型。第二個參數(shù)downstream,下游是一個收集器Collector. 這個收集器元素類型是T的子類,容器類型container為A,reduction返回值類型為D。也就是說分組的K通過分類器提供,分組的value則通過第二個參數(shù)的收集器reduce出來。正好,上個demo的源碼為:

public static<T, K> Collector<T, ?, Map<K, List<T>>>

??groupingBy(Function<? superT, ? extendsK> classifier) {

????returngroupingBy(classifier, toList());

??}

將toList當作reduce收集器,最終收集的結果是一個List<Dish>, 所以分組結束的value類型是List<Dish>。那么,可以類推value類型取決于reduce收集器,而reduce收集器則有千千萬。比如,我想對value再次分組,分組也是一種reduce。

//多級分組

Map<Type, Map<CaloricLevel, List<Dish>>> byTypeAndCalory = dishes.stream().collect(

??groupingBy(Dish::getType, groupingBy(this::getCaloricLevel)));

byTypeAndCalory.forEach((type, byCalory) -> {

?System.out.println("----------------------------------");

?System.out.println(type);

?byCalory.forEach((level, dishList) -> {

??System.out.println("\t"+ level);

??System.out.println("\t\t"+ dishList);

?});

});

驗證結果為:

----------------------------------

FISH

? DIET

? ? [Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]

? NORMAL

? ? [Dish(name=salmon, vegetarian=false, calories=450, type=FISH)]

----------------------------------

MEAT

? FAT

? ? [Dish(name=pork, vegetarian=false, calories=800, type=MEAT)]

? DIET

? ? [Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]

? NORMAL

? ? [Dish(name=beef, vegetarian=false, calories=700, type=MEAT)]

----------------------------------

OTHER

? DIET

? ? [Dish(name=rice, vegetarian=true, calories=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calories=120, type=OTHER)]

? NORMAL

? ? [Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]

總結:groupingBy的核心參數(shù)為K生成器,V生成器。V生成器可以是任意類型的收集器Collector。

比如,V生成器可以是計算數(shù)目的, 從而實現(xiàn)了sql語句中的select count(*) from table A group by Type

Map<Type, Long> typesCount = dishes.stream().collect(groupingBy(Dish::getType, counting()));

System.out.println(typesCount);

-----------

{FISH=2, MEAT=3, OTHER=4}

sql查找分組最高分select MAX(id) from table A group by Type

Map<Type, Optional<Dish>> mostCaloricByType = dishes.stream()

????.collect(groupingBy(Dish::getType, maxBy(Comparator.comparingInt(Dish::getCalories))));

這里的Optional沒有意義,因為肯定不是null。那么只好取出來了。使用collectingAndThen

Map<Type, Dish> mostCaloricByType = dishes.stream()

??.collect(groupingBy(Dish::getType,

????collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));

到這里似乎結果出來了,但IDEA不同意,編譯黃色報警,按提示修改后變?yōu)椋?/p>

Map<Type, Dish> mostCaloricByType = dishes.stream()

??.collect(toMap(Dish::getType, Function.identity(),

????BinaryOperator.maxBy(comparingInt(Dish::getCalories))));

是的,groupingBy就變成toMap了,key還是Type,value還是Dish,但多了一個參數(shù)??!這里回應開頭的坑,開頭的toMap演示是為了容易理解,真那么用則會被搞死。我們知道把一個List重組為Map必然會面臨k相同的問題。當K相同時,v是覆蓋還是不管呢?前面的demo的做法是當k存在時,再次插入k則直接拋出異常:

java.lang.IllegalStateException: Duplicate key Dish(name=pork, vegetarian=false, calories=800, type=MEAT)

??at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)

正確的做法是提供處理沖突的函數(shù),在本demo中,處理沖突的原則就是找出最大的,正好符合我們分組求最大的要求。(真的不想搞Java8函數(shù)式學習了,感覺到處都是性能問題的坑)

繼續(xù)數(shù)據(jù)庫sql映射,分組求和select sum(score) from table a group by Type

Map<Type, Integer> totalCaloriesByType = dishes.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));

然而常常和groupingBy聯(lián)合使用的另一個收集器是mapping方法生成的。這個方法接收兩個參數(shù):一個函數(shù)對流中的元素做變換,另一個則將變換的結果對象收集起來。其目的是在累加之前對每個輸入元素應用一個映射函數(shù),這樣就可以讓接收特定類型元素的收集器適應不同類型的對象。我么來看一個使用這個收集器的實際例子。比如你想得到,對于每種類型的Dish,菜單中都有哪些CaloricLevel。我們可以把groupingBy和mapping收集器結合起來,如下所示:

Map<Type, Set<CaloricLevel>> caloricLevelsByType = dishes.stream().collect(groupingBy(Dish::getType, mapping(this::getCaloricLevel, toSet())));

這里的toSet默認采用的HashSet,也可以手動指定具體實現(xiàn)toCollection(HashSet::new)

分區(qū)

分區(qū)是分組的特殊情況:由一個謂詞(返回一個布爾值的函數(shù))作為分類函數(shù),它稱為分區(qū)函數(shù)。分區(qū)函數(shù)返回一個布爾值,這意味著得到的分組Map的鍵類型是Boolean,于是它最多可以分為兩組:true or false. 例如,如果你是素食者,你可能想要把菜單按照素食和非素食分開:

Map<Boolean, List<Dish>> partitionedMenu = dishes.stream().collect(partitioningBy(Dish::isVegetarian));

當然,使用filter可以達到同樣的效果:

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

分區(qū)相對來說,優(yōu)勢就是保存了兩個副本,當你想要對一個list分類時挺有用的。同時,和groupingBy一樣,partitioningBy一樣有重載方法,可以指定分組value的類型。

Map<Boolean, Map<Type, List<Dish>>> vegetarianDishesByType = dishes.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));

Map<Boolean, Integer> vegetarianDishesTotalCalories = dishes.stream().collect(partitioningBy(Dish::isVegetarian, summingInt(Dish::getCalories)));

Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = dishes.stream().collect(partitioningBy(Dish::isVegetarian,collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));

作為使用partitioningBy收集器的最后一個例子,我們把菜單數(shù)據(jù)模型放在一邊,來看一個更加復雜也更為有趣的例子:將數(shù)組分為質(zhì)數(shù)和非質(zhì)數(shù)。

首先,定義個質(zhì)數(shù)分區(qū)函數(shù):

private booleanisPrime(intcandidate) {

??int candidateRoot = (int) Math.sqrt((double) candidate);

??return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);

}

然后找出1到100的質(zhì)數(shù)和非質(zhì)數(shù)

Map<Boolean, List<Integer>> partitionPrimes = IntStream.rangeClosed(2, 100).boxed().collect(partitioningBy(this::isPrime));

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容