Java 8收集器指南

1.概述

在本教程中,我們將介紹Java 8的收集器,這些收集器將在處理Stream的最後一步中使用。

如果您想了解有關Stream API本身的更多信息,請查看本文。

如果要查看如何利用收集器的功能進行並行處理,請檢查此項目。

2. Stream.collect()方法

Stream.collect()是Java 8的Stream API的終端方法之一。它允許我們對Stream實例中保存的數據元素執行可變的折疊操作(將元素重新打包到某些數據結構並應用一些其他邏輯,將它們串聯等)。

該操作的策略是通過Collector接口實現提供的。

3. Collectors

所有預定義的實現都可以在Collectors類中找到。通常的做法是將以下靜態導入與它們結合使用,以提高可讀性:

import static java.util.stream.Collectors.*;

或您選擇的單個導入收集器:

import static java.util.stream.Collectors.toList;

 import static java.util.stream.Collectors.toMap;

 import static java.util.stream.Collectors.toSet;

在以下示例中,我們將重用以下列表:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1。 Collectors.toList()

ToList收集器可用於將所有Stream元素收集到List實例中。要記住的重要一點是,我們不能使用此方法來假設任何特定的List實現。如果要對此進行更多控制,請改用toCollection

讓我們創建一個代表一系列元素的Stream實例,並將它們收集到一個List實例中:

List<String> result = givenList.stream()

 .collect(toList());

3.1.1。 Collectors.toUnmodifiableList()

Java 10引入了一種方便的方法來將Stream元素累積到不可修改的List

List<String> result = givenList.stream()

 .collect(toUnmodifiableList());

如果現在嘗試修改result List ,我們將得到UnsupportedOperationException

assertThatThrownBy(() -> result.add("foo"))

 .isInstanceOf(UnsupportedOperationException.class);

3.2。 Collectors.toSet()

ToSet收集器可用於將所有Stream元素收集到Set實例中。要記住的重要一點是,我們不能使用此方法假定任何特定的Set實現。如果要對此進行更多控制,可以使用toCollection

讓我們創建一個代表一系列元素的Stream實例,並將它們收集到Set實例中:

Set<String> result = givenList.stream()

 .collect(toSet());

Set不包含重複的元素。如果我們的集合包含彼此相等的元素,則它們僅在結果Set出現一次:

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");

 Set<String> result = listWithDuplicates.stream().collect(toSet());

 assertThat(result).hasSize(4);

3.2.1。 Collectors.toUnmodifiableSet()

從Java 10開始,我們可以使用toUnmodifiableSet()收集器輕鬆創建一個不可修改的Set

Set<String> result = givenList.stream()

 .collect(toUnmodifiableSet());

任何修改result Set嘗試都將以UnsupportedOperationException結尾:

assertThatThrownBy(() -> result.add("foo"))

 .isInstanceOf(UnsupportedOperationException.class);

3.3。 Collectors.toCollection()

您可能已經註意到,在使用toSet and toList收集器時,您無法對其實現進行任何假設。如果要使用自定義實現,則需要將toCollection收集器與您選擇的提供的收集一起使用。

讓我們創建一個代表一系列元素的Stream實例,並將它們收集到LinkedList實例中:

List<String> result = givenList.stream()

 .collect(toCollection(LinkedList::new))

請注意,這不適用於任何不可變的集合。在這種情況下,您將需要編寫自定義的Collector實現或使用collectingAndThen

3.4。 CollectorstoMap()

ToMap收集器可用於將Stream元素收集到Map實例中。為此,我們需要提供兩個功能:

  • keyMapper
  • valueMapper

keyMapper將用於從Stream元素中提取Map鍵, valueMapper將用於提取與給定鍵關聯的值。

讓我們將這些元素收集到一個Map ,該Map將字符串存儲為鍵,並將其長度存儲為值:

Map<String, Integer> result = givenList.stream()

 .collect(toMap(Function.identity(), String::length))

Function.identity()只是用於定義接受和返回相同值的函數的快捷方式。

如果我們的集合包含重複元素,會發生什麼?與toSet相反, toMap不會靜默過濾重複項。這是可以理解的–應該如何確定該鍵選擇哪個值?

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");

 assertThatThrownBy(() -> {

 listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));

 }).isInstanceOf(IllegalStateException.class);

請注意, toMap甚至不會評估值是否也相等。如果看到重複的鍵,則會立即引發IllegalStateException

在發生鍵衝突的情況下,我們應該將toMap與另一個簽名一起使用:

Map<String, Integer> result = givenList.stream()

 .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

這裡的第三個參數是BinaryOperator ,我們可以在其中指定希望如何處理衝突。在這種情況下,我們將只選擇這兩個衝突值中的任何一個,因為我們知道相同的字符串也將始終具有相同的長度。

3.4.1。 Collectors.toUnmodifiableMap()

ListSet相似,Java 10引入了一種簡單的方法來將Stream元素收集到不可修改的Map

Map<String, Integer> result = givenList.stream()

 .collect(toMap(Function.identity(), String::length))

如我們所見,如果我們嘗試將新條目放入result Map ,則會得到UnsupportedOperationException

assertThatThrownBy(() -> result.put("foo", 3))

 .isInstanceOf(UnsupportedOperationException.class);

3.5。 Collectors .c ollectingAndThen()

CollectingAndThen是一個特殊的收集器,允許在收集結束後立即對結果執行其他操作。

讓我們將Stream元素收集到List實例,然後將結果轉換為ImmutableList實例:

List<String> result = givenList.stream()

 .collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6。 Collectors .joining()

Joining收集器可用於聯接Stream<String>元素。

我們可以通過以下方式將他們加入一起:

String result = givenList.stream()

 .collect(joining());

這將導致:

"abbcccdd"

您還可以指定自定義分隔符,前綴,後綴:

String result = givenList.stream()

 .collect(joining(" "));

這將導致:

"a bb ccc dd"

或者您可以寫:

String result = givenList.stream()

 .collect(joining(" ", "PRE-", "-POST"));

這將導致:

"PRE-a bb ccc dd-POST"

3.7。 Collectors .counting()

Counting是一個簡單的收集器,可以簡單地計數所有Stream元素。

現在我們可以寫:

Long result = givenList.stream()

 .collect(counting());

3.8。 Collectors .summaryizingDouble / Long / Int()

SummarizingDouble/Long/Int是一個收集器,它返回一個特殊類,其中包含有關提取元素Stream中數字數據的統計信息。

我們可以通過執行以下操作獲取有關字符串長度的信息:

DoubleSummaryStatistics result = givenList.stream()

 .collect(summarizingDouble(String::length));

在這種情況下,將滿足以下條件:

assertThat(result.getAverage()).isEqualTo(2);

 assertThat(result.getCount()).isEqualTo(4);

 assertThat(result.getMax()).isEqualTo(3);

 assertThat(result.getMin()).isEqualTo(1);

 assertThat(result.getSum()).isEqualTo(8);

3.9。 Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Int是一個收集器,僅返回提取元素的平均值。

我們可以通過執行以下操作來獲得平均字符串長度:

Double result = givenList.stream()

 .collect(averagingDouble(String::length));

*3.10。 Collectors .summingDouble / Long / Int()**

SummingDouble/Long/Int是僅返回所提取元素之和的收集器。

通過執行以下操作,我們可以得出所有字符串長度的總和:

Double result = givenList.stream()

 .collect(summingDouble(String::length));

3.11。 Collectors.maxBy()/minBy()

MaxBy / MinBy收集器根據提供的Comparator實例返回Stream的最大/最小元素。

我們可以通過執行以下操作來選擇最大的元素:

Optional<String> result = givenList.stream()

 .collect(maxBy(Comparator.naturalOrder()));

請注意,返回值包裝在Optional實例中。這迫使用戶重新考慮空的收集箱。

3.12。 Collectors . groupingBy()

GroupingBy收集器用於按某些屬性對對象進行分組,並將結果存儲在Map實例中。

我們可以按字符串長度對它們進行分組,並將分組結果存儲在Set實例中:

Map<Integer, Set<String>> result = givenList.stream()

 .collect(groupingBy(String::length, toSet()));

這將導致以下情況成立:

assertThat(result)

 .containsEntry(1, newHashSet("a"))

 .containsEntry(2, newHashSet("bb", "dd"))

 .containsEntry(3, newHashSet("ccc"));

注意, groupingBy方法的第二個參數是一個Collector ,您可以自由使用您選擇的任何Collector

3.13。 Collectors.partitioningBy()

PartitioningBygroupingBy一種特殊情況,它接受一個Predicate實例並將Stream元素收集到一個Map實例中,該實例將Boolean值存儲為鍵,將集合存儲為值。在“ true”鍵下,您可以找到與給定Predicate匹配的元素集合,在“ false”鍵下,您可以找到與給定Predicate不匹配的元素集合。

你可以寫:

Map<Boolean, List<String>> result = givenList.stream()

 .collect(partitioningBy(s -> s.length() > 2))

結果導致包含以下內容的地圖:

{false=["a", "bb", "dd"], true=["ccc"]}

3.14。 Collectors.teeing()

讓我們使用到目前為止學習到的收集器從給定Stream找到最大和最小數目:

List<Integer> numbers = Arrays.asList(42, 4, 2, 24);

 Optional<Integer> min = numbers.stream().collect(minBy(Integer::compareTo));

 Optional<Integer> max = numbers.stream().collect(maxBy(Integer::compareTo));

 // do something useful with min and max

在這裡,我們使用了兩個不同的收集器,然後將這兩個收集器的結果相結合以創建有意義的東西。在Java 12之前,為了涵蓋此類用例,我們必須對給定的Stream兩次操作,將中間結果存儲到臨時變量中,然後再組合這些結果。

幸運的是,Java 12提供了一個內置的收集器,可以代表我們執行這些步驟:我們要做的就是提供兩個收集器和合併器功能。

由於這種新的收藏家開杆朝兩個不同的方向給定的流,這就是所謂的teeing:

numbers.stream().collect(teeing(

 minBy(Integer::compareTo), // The first collector

 maxBy(Integer::compareTo), // The second collector

 (min, max) -> // Receives the result from those collectors and combines them

 ));

該示例可在GitHub的core-java-12項目中找到。

4.定制Collector

如果要編寫Collector實現,則需要實現Collector接口並指定其三個通用參數:

public interface Collector<T, A, R> {...}
  1. T –可用於收集的對像類型,
  2. A –可變累加器對象的類型,
  3. R –最終結果的類型。

讓我們寫一個示例收集器,將元素收集到ImmutableSet實例中。我們首先指定正確的類型:

private class ImmutableSetCollector<T>

 implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {...}

由於我們需要一個可變的集合來進行內部集合操作處理,因此我們不能為此使用ImmutableSet 。我們需要使用其他可變的集合或可以為我們臨時累積對象的任何其他類。
在這種情況下,我們將繼續使用ImmutableSet.Builder ,現在我們需要實現5種方法:

  • Supplier<ImmutableSet.Builder<T>> **supplier** ()
  • BiConsumer<ImmutableSet.Builder<T>, T> **accumulator** ()
  • BinaryOperator<ImmutableSet.Builder<T>> **combiner** ()
  • Function<ImmutableSet.Builder<T>, ImmutableSet<T>> **finisher** ()
  • Set<Characteristics> **characteristics** ()

**The supplier()**方法返回一個Supplier實例,該實例生成一個空的累加器實例,因此,在這種情況下,我們可以簡單地編寫:

@Override

 public Supplier<ImmutableSet.Builder<T>> supplier() {

 return ImmutableSet::builder;

 }

**The accumulator()**方法返回一個函數,該函數用於將新元素添加到現有的accumulator像中,因此我們僅使用Builderadd方法。

@Override

 public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {

 return ImmutableSet.Builder::add;

 }

**The combiner()**方法返回一個函數,該函數用於將兩個累加器合併在一起:

@Override

 public BinaryOperator<ImmutableSet.Builder<T>> combiner() {

 return (left, right) -> left.addAll(right.build());

 }

**The finisher()**方法返回一個函數,該函數用於將累加器轉換為最終結果類型,因此在這種情況下,我們將僅使用Builderbuild方法:

@Override

 public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {

 return ImmutableSet.Builder::build;

 }

**The characteristics()**方法用於為Stream提供一些其他信息,這些信息將用於內部優化。在這種情況下,我們不注意Set的元素順序,因此我們將使用Characteristics.UNORDERED 。要獲取有關此主題的更多信息,請選中“ Characteristics ”'JavaDoc。

@Override public Set<Characteristics> characteristics() {

 return Sets.immutableEnumSet(Characteristics.UNORDERED);

 }

這是完整的實現及其用法:

public class ImmutableSetCollector<T>

 implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {



 @Override

 public Supplier<ImmutableSet.Builder<T>> supplier() {

 return ImmutableSet::builder;

 }



 @Override

 public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {

 return ImmutableSet.Builder::add;

 }



 @Override

 public BinaryOperator<ImmutableSet.Builder<T>> combiner() {

 return (left, right) -> left.addAll(right.build());

 }



 @Override

 public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {

 return ImmutableSet.Builder::build;

 }



 @Override

 public Set<Characteristics> characteristics() {

 return Sets.immutableEnumSet(Characteristics.UNORDERED);

 }



 public static <T> ImmutableSetCollector<T> toImmutableSet() {

 return new ImmutableSetCollector<>();

 }

在這裡:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dddd");



 ImmutableSet<String> result = givenList.stream()

 .collect(toImmutableSet());

5.結論

在本文中,我們深入探討了Java 8的收集器,並展示瞭如何實現它。確保檢查我的一個項目,該項目可以增強Java並行處理的功能。

所有代碼示例都可以在GitHub上找到。您可以在我的網站上閱讀更多有趣的文章。