日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java Streams,第 2 部分: 使用流执行聚合

發布時間:2025/3/21 java 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java Streams,第 2 部分: 使用流执行聚合 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

“累加器反模式”

第 1 部分中的第 1 個例子使用 Streams 執行了一次簡單的匯總,如清單 1 所示。

清單 1. 使用 Streams 聲明性地計算聚合值

int totalSalesFromNY= txns.stream().filter(t -> t.getSeller().getAddr().getState().equals("NY")).mapToInt(t -> t.getAmount()).sum();

清單 2 展示了如何采用 “老方法” 編寫這個示例。

清單 2. 通過命令計算同一個聚合值

int sum = 0; for (Txn t : txns) {if (t.getSeller().getAddr().getState().equals("NY"))sum += t.getAmount(); }

第 1 部分介紹了盡管新方法比老方法更長,但新方法更可取的一些原因:

應用這個特殊的聚合是有一些額外原因的。清單 2?演示了累加器反模式,其中代碼首先聲明并初始化一個可變累加

器變量,然后繼續在循環中更新累加器。為什么這樣做是不正確的?首先,此代碼樣式難以并行化。沒有協調(比如

同步),對累加器的每次訪問都導致一次數據爭用(而借助協調,協調導致的爭用會破壞并行性所帶來的效率收益)。

代碼更加清晰,因為它被簡單地構造為一些簡單操作的組合。

?

  • 該代碼是通過聲明進行表達的(描述想要的結果),而不是通過命令進行表達的(一個計算結果的循序漸進的過程)。
  • 隨著表達的查詢變得更加復雜,此方法可以更干凈地擴展。


所以,如果命令式累加是錯誤的工具,那什么才是正確的工具?在這個特定的問題中,您已經看到了答案的線索

(sum()?方法),但這只是一個強大的、通用的縮減?技術的一種特殊情況。縮減技術簡單、靈活,而且可并行化,

還能在比命令式累加更高的抽象級別上操作。累加器方法更不可取的另一個原因是,它在太低的級別上建模計算 —

?在各個元素的級別上,而不是在整個數據集的級別上。與 “逐個依次迭代交易金額,將每筆金額添加到一個已初始化

為 0 的累加器” 相比,“所有交易金額的總和” 是目標的更抽象、更直接的陳述。

?

縮減

?

?

縮減(也稱為折疊)是一種來自函數編程的技術,它抽象化了許多不同的累加操作。給定一個類型為 T,包含 x 個元素的非空數列 X1, x2, ..., xn?和 T 上的二元運算符(在這里表示為 *),* 下的 X 的縮減?被定義為:? ?(x1?* x2?* ...* xn)

當使用普通的加法作為二元運算符來應用于某個數列時,縮減就是求和。但其他許多操作也可以使用縮減來描述。

如果二元運算符是 “獲取兩個元素中較大的一個”(這在 Java 中可以使用拉姆達表達式?(x,y) -> Math.max(x,y)

?來表示,或者更簡單地表示為方法引用Math::max),則縮減對應于查找最大值。

通過將累加描述為縮減,而不使用累加器反模式,可以采用更抽象、更緊湊、更并行化?的方式來描述計算 — 只要您

的二元運算符滿足一個簡單條件:結合性。回想一下,如果 a、b 和 c 元素滿足以下條件,二元運算符 * 就是結合性

的:? ?((a * b) * c) = (a * (b * c))

結合性意味著分組無關緊要。如果二元運算符是結合性的,那么可以按照任何順序安全地執行縮減。在順序執行中,

執行的自然順序是從左向右;在并行執行中,數據劃分為分段,分別縮減每個分段,然后組合結果。結合性可確保這

兩種方法得到相同的答案。如果將結合性的定義擴展到 4 項,可能更容易理解:

? ?(((a * b) * c) * d) = ((a * b) * (c * d))

左側對應于典型的順序計算;右側對應于表示典型的并行執行的分區執行,其中輸入序列被分解為幾部分,各部分并

行縮減,并使用 * 將各部分的結果組合起來。(或許令人驚奇的是,* 不需要是可交換的,但許多運算符通常都可用

于縮減,比如相加和求最大值等。具有結合性但沒有可交換性的二元運算符的一個例子是字符串串聯。)

Streams 庫有多種縮減方法,包括:

Optional<T> reduce(BinaryOperator<T> op) T reduce(T identity, BinaryOperator<T> op)

在這些方法中,最簡單的方法僅獲得一個結合性二元運算符,在該運算符下計算流元素的縮減結果。結果被描述為

?Optional;如果輸入流是空的,則縮減結果也是空的。(如果輸入只有一個元素,那么縮減結果就是該元素。)

如果您有一個字符串集合,您可以將這些元素的串聯計算為:

String concatenated = strings.stream().reduce("", String::concat);

對于這兩種方法中的第二種方法,您需要提供一個身份值,在字符串為空時,還可以將該值用作結果。身份值必須滿

足所有?x?的限制:

? ?身份 * x = x

x * 身份 = x

不是所有二元運算符都有身份值,但當它們擁有身份值時,它們可能不會得到您想要的結果。例如,計算最大值時,

您可能傾向于使用值Integer.MIN_VALUE?作為身份(它確實滿足要求)。但在空流上使用該身份時,結果可能不是

您想要的;您無法確定空輸入和僅包含Integer.MIN_VALUE?的非空輸入之間的區別。(有時這不是問題,但有時會

導致問題 — 因此 Streams 庫將留給客戶,由客戶決定是否指定身份。)

對于字符串串聯,身份是空字符串,所以您可以將前面的示例重寫為:

String concatenated = strings.stream().reduce("", String::concat);

類似地,您可以將數組上的整數總和描述為:

int sum = Stream.of(ints).reduce(0, (x,y) -> x+y);

(但實際上,您使用了?IntStream.sum()?便捷方法。)

縮減不需要僅應用于整數和字符串,它可以應用于您想要將元素序列縮減為該類型的單個元素的任何情形。例如,您可以

通過縮減來計算最高的人:

Comparator<Person> byHeight = Comparators.comparingInt(Person::getHeight); BinaryOperator<Person> tallerOf = BinaryOperator.maxBy(byHeight); Optional<Person> tallest = people.stream().reduce(tallerOf);

如果提供的二元運算符不是結合性的,或者提供的身份值實際上不是該二元運算符的身份,那么在并行執行該操作時,結

果可能是錯的,而且同一個數據集上的不同執行過程可能會生成不同的結果。

?

可變縮減

縮減獲取一個值序列并將它縮減為單個值,比如數列的和或它的最大值。但是有時您不想要單個匯總值;您想將結果組織

為類似?List?或?Map的數據結構,或者將它縮減為多個匯總值。在這種情況下,您應該使用縮減?的可變類似方法,也稱為

收集。

考慮將元素累積到一個?List?中的簡單情況。使用累加器反模式,您可以這樣編寫它:

ArrayList<String> list = new ArrayList<>(); for (Person p : people)list.add(p.toString());

當累加器變量是一個簡單值時,縮減是累加的更好替代方法,與此類似,在累加器結果是更復雜的數據結構時,也有一種

更好的替代方法。縮減的構建塊是一個身份值和一種將兩個值組合成新值的途徑;可變縮減的類似方法包括:

  • 一種生成空結果容器的途徑
  • 一種將新元素合并到結果容器中的途徑
  • 一種合并兩個結果容器的途徑

這些構建塊可以輕松地表達為函數。這些函數中的第 3 個支持并行執行可變縮減:您可以對數據集進行分區,為每一部分

生成一個中間累加結果,然后合并中間結果。Streams 庫有一個?collect()?方法,它接受以下 3 個函數:

<R> collect(Supplier<R> resultSupplier,BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)

在前一節中,您看到了一個使用縮減來計算字符串串聯的示例。該代碼會生成正確的結果,但是,因為 Java 中的字符串

是不可變的,而且串聯要求復制整個字符串,所以它還有?O(n2)?運行時(一些字符串將復制多次)。您可以通過將結果

收集到?StringBuilder?中,更高效地表達字符串串聯:

StringBuilder concat = strings.stream().collect(() -> new StringBuilder(),(sb, s) -> sb.append(s),(sb, sb2) -> sb.append(sb2));

此方法使用?StringBuilder?作為結果容器。傳遞給?collect()?的 3 個函數使用默認構造函數創建了一個空容

器,append(String)?方法將一個元素添加到容器中,append(StringBuilder)?方法將一個容器合并到另一個容器

中。使用方法引用可能可以比拉姆達表達式更好地表達此代碼:

StringBuilder concat = strings.stream().collect(StringBuilder::new,StringBuilder::append,StringBuilder::append);

類似地,要將一個流收集到一個?HashSet?中,您可以這樣做:

Set<String> uniqueStrings = strings.stream().collect(HashSet::new,HashSet::add,HashSet::addAll);

在這個版本中,結果容器是一個?HashSet?而不是?StringBuilder,但方法是一樣的:使用默認構造函數創建一個新

的結果容器,使用add()?方法將一個新元素合并到結果集中,使用?addAll()?方法合并兩個結果集。很容易看到如何

將此代碼調整為其他任何類型的集合。

您可能會想,因為使用了可變結果容器(StringBuilder?或?HashSet),所以這也是累加器反模式的一個例子。但

其實不然。累加器反模式在這種情況下采用的類似方法是:

Set<String> set = new HashSet<>(); strings.stream().forEach(s -> set.add(s));


就像只要組合函數是結合性的,且沒有相互干擾的副作用,就可以安全地并行化縮減一樣,如果滿足一些簡單的一致性

要求(在?collect()?的規范中列出),就可以安全地并行化使用了Stream.collect()?的可變縮減。關鍵區別在于,

對于?forEach()?版本,多個線程會同時嘗試訪問一個結果容器,而對于并行?collect(),每個線程擁有自己的本地結

果容器,會在以后合并其中的結果。

?

收集器

傳遞給?collect()?的 3 個函數(創建、填充和合并結果容器)之間的關系非常重要,所以有必要提供它自己的抽象

?Collector?和collect()?的相應簡化版本。字符串串聯示例可重寫為:

String concat = strings.stream().collect(Collectors.joining());

收集到結果集的示例可重寫為:

Set<String> uniqueStrings = strings.stream().collect(Collectors.toSet());

Collectors?類包含許多常見聚合操作的因素,比如累加到集合中、字符串串聯、縮減和其他匯總計算,以及創建匯

總表(通過groupingBy())。表 1 包含部分內置收集器的列表,而且如果它們不夠用,編寫您自己的收集器也很容

易(請參閱 “自定義收集器” 部分)。

表 1. 內置收集器

收集器行為
toList()將元素收集到一個?List?中。
toSet()將元素收集到一個?Set?中。
toCollection(Supplier<Collection>)將元素收集到一個特定類型的?Collection?中。
toMap(Function<T, K>, Function<T, V>)將元素收集到一個?Map?中,依據提供的映射函數將元素轉換為鍵值。
summingInt(ToIntFunction<T>)計算將提供的?int?值映射函數應用于每個元素(以及?long?和?double?版本)的結果的總和。
summarizingInt(ToIntFunction<T>)計算將提供的?int?值映射函數應用于每個元素(以及?long?和?double?版本)的結果的sum、min、max、count?和?average。
reducing()向元素應用縮減(通常用作下游收集器,比如用于?groupingBy)(各種版本)。
partitioningBy(Predicate<T>)將元素分為兩組:為其保留了提供的預期的組和未保留預期的組。
partitioningBy(Predicate<T>, Collector)將元素分區,使用指定的下游收集器處理每個分區。
groupingBy(Function<T,U>)將元素分組到一個?Map?中,其中的鍵是所提供的應用于流元素的函數,值是共享該鍵的元素列表。
groupingBy(Function<T,U>, Collector)將元素分組,使用指定的下游收集器來處理與每個組有關聯的值。
minBy(BinaryOperator<T>)計算元素的最小值(與?maxBy()?相同)。
mapping(Function<T,U>, Collector)將提供的映射函數應用于每個元素,并使用指定的下游收集器(通常用作下游收集器本身,比如用于?groupingBy)進行處理。
joining()假設元素為?String?類型,將這些元素聯結到一個字符串中(或許使用分隔符、前綴和后綴)。
counting()計算元素數量。(通常用作下游收集器。)

將收集器函數分組到?Collector?抽象中在語法上更簡單,但實際收益來自您開始將收集器組合在一起時,比如您想要

創建復雜的匯總結果(比如?groupingBy()?收集器創建的摘要)的時候,該收集器依據來自元素的一個鍵將元素收集

到?Map?中。例如,要創建超過 1000 美元的交易的Map,可以使用賣家作為鍵:

Map<Seller, List<Txn>> bigTxnsBySeller =txns.stream().filter(t -> t.getAmount() > 1000).collect(groupingBy(Txn::getSeller));

但是,假設您不想要每個賣家的交易?List,而想要來自每個賣家的最大交易。您仍希望使用賣家作為結果的鍵,但您

希望進一步處理與賣家關聯的交易,以便將它縮減為最大的交易。可以使用?groupingBy()?的替代版本,無需將每個

鍵的元素收集到列表中,而是將它們提供給另一個收集器(downstream?收集器)。對于下游收集器,您可以選擇?

maxBy()?等縮減方法:

Map<Seller, Txn> biggestTxnBySeller =txns.stream().collect(groupingBy(Txn::getSeller,maxBy(comparing(Txn::getAmount))));maxBy(comparing(Txn::getAmount))));

在這里,您將交易分組到以賣家作為鍵的映射中,但該映射的值是使用?maxBy()?收集器收集該賣家的所有銷售的結果。

如果您不想要該賣家的最大交易,而想要總和,可以使用?summingInt()?收集器:

Map<Seller, Integer> salesBySeller =txns.stream().collect(groupingBy(Txn::getSeller,summingInt(Txn::getAmount)));

要獲得多級匯總結果,比如每個區域和賣家的銷售,可以使用另一個?groupingBy?收集器作為下游收集器:

Map<Region, Map<Seller, Integer>> salesByRegionAndSeller =txns.stream().collect(groupingBy(Txn::getRegion,groupingBy(Txn::getSeller, summingInt(Txn::getAmount))));

舉一個不同領域的例子:要計算一個文檔中的詞頻直方圖,可以使用?BufferedReader.lines()?將文檔拆分為行,

使用Pattern.splitAsStream()?將它分解為一個單詞流,然后使用?collect()?和?groupingBy()?創建一個?Map,

后者的鍵是單詞,值是這些單詞的數量,如清單 3 所示。

清單 3. 使用 Streams 計算單詞數量直方圖

Pattern whitespace = Pattern.compile("\\s+");Map<String, Integer> wordFrequencies =reader.lines().flatMap(s -> whitespace.splitAsStream()).collect(groupingBy(String::toLowerCase),Collectors.counting());

?

自定義收集器

盡管 JDK 提供的標準的收集器集合非常大,但編寫您自己的收集器非常容易。Collector?接口(如清單 4 所示)非常

簡單。該接口通過 3 種類型來實現參數化:輸入類型?T、累加器類型?A?和最終的返回類型?R(A?和?R?通常是相同的),

這些方法返回的函數與之前演示的?collect()3 參數版本所接受的函數類似。

清單 4.?Collector?接口

public interface Collector<T, A, R> {/** Return a function that creates a new empty result container */Supplier<A> supplier();/** Return a function that incorporates an element into a container */BiConsumer<A, T> accumulator();/** Return a function that merges two result containers */BinaryOperator<A> combiner();/** Return a function that converts the intermediate result containerinto the final representation */Function<A, R> finisher();/** Special characteristics of this collector */Set<Characteristics> characteristics(); }

Collectors?中的大部分收集器工廠的實現都很簡單。例如,toList()?的實現是:

return new CollectorImpl<>(ArrayList::new,List::add,(left, right) -> { left.addAll(right); return left; },CH_ID);

此實現使用?ArrayList?作為結果容器,使用?add()?合并一個元素,并使用?addAll()?將一個列表合并到另一個中,

通過這些特征表明它的完成函數是身份函數(這使得流框架可以優化執行)。

與之前看到的一樣,一些一致性要求與縮減中的身份和累加器函數之間的限制類似。這些要求已在?Collector?的規

范中列出。

作為一個更復雜的示例,可以考慮在數據集上創建匯總統計數據的問題。很容易使用縮減來計算數字數據集的總和、

最小值、最大值或數量(而且您可以使用總和和數量來計算平均值)。在數據上,使用縮減在一輪計算中一次性計算所

有這些結果更加困難。但您可以輕松地編寫一個Collector?來高效地(如果愿意,還可并行地)執行此計算。

Collectors?類包含一個?collectingInt()?工廠方法,該方法返回一個?IntSummaryStatistics,后者會執行您

想要的準確操作,比如在一輪計算中計算?sum、min、max、count?和?average。IntSummaryStatistics?的實現

很簡單,而且您可以輕松地編寫自己的類似收集器來計算任意數據匯總結果(或擴展此結果)。

清單 5 顯示了?IntSummaryStatistics?類。實際實現包含更多細節(包含用于獲取匯總統計數據的 getter),但它

的核心是簡單的accept()?和?combine()?方法。

清單 5.?summarizingInt()?收集器使用的?IntSummaryStatistics?類

public class IntSummaryStatistics implements IntConsumer {private long count;private long sum;private int min = Integer.MAX_VALUE;private int max = Integer.MIN_VALUE;public void accept(int value) {++count;sum += value;min = Math.min(min, value);max = Math.max(max, value);}public void combine(IntSummaryStatistics other) {count += other.count;sum += other.sum;min = Math.min(min, other.min);max = Math.max(max, other.max);}// plus getters for count, sum, min, max, and average}

如您所見,這是一個非常簡單的類。在觀察每個新數據元素時,會以各種方式更新各種匯總結果,而且會以各種方

式組合兩個IntSummaryStatistics?持有者。Collectors.summarizingInt()?的實現(如清單 6 所示)同樣

很簡單;它創建了一個?Collector,以便通過應用一個整數值來提取器函數,并將結果傳遞給?IntSummaryStatistics.accept()?來合并一個元素。

清單 6.?summarizingInt()?收集器工廠

public static <T>Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) {return new CollectorImpl<T, IntSummaryStatistics, IntSummaryStatistics>(IntSummaryStatistics::new,(r, t) -> r.accept(mapper.applyAsInt(t)),(l, r) -> { l.combine(r); return l; },CH_ID);}

組合收集器的容易性(您在?groupingBy()?示例中已看到)和創建新收集器的容易性相結合,可以創建流數據的幾乎

任何匯總結果,同時保持您的代碼緊湊而又清晰。

?

第 2 部分的小結

聚合工具是 Streams 庫的最有用和靈活的部分之一。可以使用縮減操作來輕松地按順序或并行聚合簡單的值;更復雜的

匯總結果可通過collect()?創建。該庫附帶了一組簡單的基本收集器,可以組合它們來執行更復雜的聚合,而且您可以

輕松地將自己的收集器添加到組合中。

在?第 3 部分?中,將深入剖析 Streams 的內部結構,以便了解在性能至關重要時如何最高效地使用該庫。

參考資料

學習

  • java.util.stream?的?包文檔:查看該庫的工作原理概述。
  • Java 中的函數編程。利用 Lambda 表達式的強大功能(Venkat Subramaniam,Pragmatic Bookshelf,2014 年):閱讀對 Java 8 語言和庫特性背后的函數編程概念的詳細介紹。
  • 精通拉姆達表達式:多核世界中的 Java 編程(Maurice Naftalin,McGraw-Hill Education,2014):了解對 Java 8 語言和庫特性的詳細分析和基本原理。
  • 我應返回集合還是流?:在這個 Stack Overflow 答案中,查找在 API 簽名中使用流的指導原則。
  • developerWorks Java 技術專區:這里有數百篇關于 Java 編程各個方面的文章。

獲得產品和技術

  • RxJava 庫:了解一個類似 Streams 的反應式庫,該庫為?java.util.stream?的功能提供了補充。

討論

  • 加入?developerWorks 中文社區,查看開發人員推動的博客、論壇、組和維基,并與其他 developerWorks 用戶交流。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

總結

以上是生活随笔為你收集整理的Java Streams,第 2 部分: 使用流执行聚合的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。