博客 / 詳情

返回

從頭學Java17-Stream API(二)結合Record、Optional

Stream API

Stream API 是按照map/filter/reduce方法處理內存中數據的最佳工具。
本系列教程由Record講起,然後結合Optional,討論collector的設計。

使用Record對不可變數據進行建模

Java 語言為您提供了幾種創建不可變類的方法。可能最直接的是創建一個包含final字段的final類。下面是此類的示例。

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

編寫這些元素後,需要為字段添加訪問器。您還將添加一個 toString)() 方法,可能還有一個 equals)() 以及一個 hashCode()) 方法。手寫所有這些非常乏味且容易出錯,幸運的是,您的 IDE 可以為您生成這些方法。

如果需要通過網絡或文件系統將此類的實例從一個應用程序傳送到另一個應用程序,則還可以考慮使此類可序列化。如果這樣做,還要添加一些有關如何序列化的信息。JDK 為您提供了幾種控制序列化的方法。

最後,您的Point類可能有一百多行,主要是IDE 生成的代碼,只是為了對需要寫入文件的兩個整數不可變集進行建模。

Record已經添加到 JDK 以改變這一切。只需一行代碼即可為您提供所有這些。您需要做的就是聲明record的狀態;其餘部分由編譯器為您生成。

呼叫Record支援

Record可幫助您使此代碼更簡單。從 Java SE 14 開始,您可以編寫以下代碼。

public record Point(int x, int y) {}

這一行代碼為您創建以下元素。

  1. 它是一個不可變的類,有兩個字段:xy
  2. 它有一個標準的構造函數,用於初始化這兩個字段。
  3. toString)()、equals)() 和 hashCode()) 方法是由編譯器為您創建的,其默認行為與 IDE 將生成的內容相對應。如果需要,可以通過添加自己的實現來修改此行為。
  4. 它可以實現Serializable接口,以便您可以通過網絡或通過文件系統發送到其他應用程序。序列化和反序列化record的方式遵循本教程末尾介紹的一些特殊規則。

record使創建不可變的數據集變得更加簡單,無需任何 IDE 的幫助。降低了錯誤的風險,因為每次修改record的組件時,編譯器都會自動更新 equals()) 和 hashCode()) 方法。

record的類

record也是類,是用關鍵字record而不是class聲明的類。讓我們聲明以下record。

public record Point(int x, int y) {}

編譯器在創建record時為您創建的類是final的。

此類繼承了 java.lang.Record 類。因此,您的record不能繼承其他任何類。

一條record可以實現任意數量的接口。

聲明record的組成部分

緊跟record名稱的塊是(int x, int y) 。它聲明瞭record組件。對於record的每個組件,編譯器都會創建一個同名的私有final字段。您可以在record中聲明任意數量的組件。

除了字段,編譯器還為每個組件生成一個訪問器。此訪問器跟組件的名稱相同,並返回其值。對於此record,生成的兩個方法如下。

public int x() {
    return this.x;
}

public int y() {
    return this.y;
}

如果此實現適用於您的應用程序,則無需添加任何內容。不過,也可以定義自己的訪問器。

編譯器為您生成的最後一個元素是 Object 類中 toString()、)equals)() 和 hashCode()) 方法的重寫。如果需要,您可以定義自己對這些方法的覆蓋。

無法添加到record的內容

有三件事不能添加到record中:

  1. 額外聲明的實例字段。不能添加任何與組件不對應的實例字段。
  2. 實例字段的初始化。
  3. 實例的初始化塊。

您可以使用靜態字段,靜態初始化塊。

使用標準構造函數構造record

編譯器還會為您創建一個構造函數,稱為標準構造函數 canonical constructor。此構造函數以record的組件作為參數,並將其值複製到字段中。

在某些情況下,您需要覆蓋此默認行為。讓我們研究兩種情況:

  1. 您需要驗證組件的狀態
  2. 您需要製作可變組件的副本。

使用緊湊構造函數

可以使用兩種不同的語法來重新定義record的標準構造函數。可以使用緊湊構造函數或標準構造函數本身。

假設您有以下record。

public record Range(int start, int end) {}

對於該名稱的record,應該預期 end大於start .您可以通過在record中編寫緊湊構造函數來添加驗證規則。

public record Range(int start, int end) {

    public Range {//不需要參數塊
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
    }
}

緊湊構造函數不需要聲明其參數塊。

請注意,如果選擇此語法,則無法直接分配record的字段,例如this.start = start - 這是通過編譯器添加代碼為您完成的。 但是,您可以為參數分配新值,這會導致相同的結果,因為編譯器生成的代碼隨後會將這些新值分配給字段。

public Range {
    // set negative start and end to 0
    // by reassigning the compact constructor's
    // implicit parameters
    if (start < 0)
        start = 0;//無法給this.start賦值
    if (end < 0)
        end = 0;
}

使用標準構造函數

如果您更喜歡非緊湊形式(例如,因為您不想重新分配參數),則可以自己定義標準構造函數,如以下示例所示。

public record Range(int start, int end) {//跟緊湊構造不能共存

    public Range(int start, int end) {
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
        if (start < 0) {
            this.start = 0;
        } else {
            this.start = start;
        }
        if (end > 100) {
            this.end = 10;
        } else {
            this.end = end;
        }
    }
}

這種情況下,您編寫的構造函數需要為record的字段手動賦值。

如果record的組件是可變的,則應考慮在標準構造函數和訪問器中製作它們的副本。

自定義構造函數

還可以向record添加自定義構造函數,只要此構造函數內調用record的標準構造函數即可。語法與經典語法相同。對於任何類,調用this()必須是構造函數的第一個語句。

讓我們檢查以下Staterecord。它由三個組件定義:

  1. 此州的名稱
  2. 該州首府的名稱
  3. 城市名稱列表,可能為空。

我們需要存儲城市列表的副本,確保它不會從此record的外部修改。 這可以通過使用緊湊形式,將參數重新分配給副本。

擁有一個不用城市作參數的構造函數在您的應用程序中很有用。這可以是另一個構造函數,它只接收州名和首都名。第二個構造函數必須調用標準構造函數。

然後,您可以將城市作為 vararg 傳遞。為此,您可以創建第三個構造函數。

public record State(String name, String capitalCity, List<String> cities) {

    public State {
        // List.copyOf returns an unmodifiable copy,
        // so the list assigned to `cities` can't change anymore
        cities = List.copyOf(cities);
    }

    public State(String name, String capitalCity) {
        this(name, capitalCity, List.of());
    }

    public State(String name, String capitalCity, String... cities) {
        this(name, capitalCity, List.of(cities));//也是不可變的
    }

}

請注意,List.copyOf()) 方法的參數不接受空值。

獲取record的狀態

您不需要向record添加任何訪問器,因為編譯器會為您執行此操作。一條record的每個組件都有一個訪問器方法,該方法具有此組件的名稱。

但是,某些情況下,您需要定義自己的訪問器。 例如,假設上一節中的Staterecord在構造期間沒有創建列表的不可修改的副本 - 那麼它應該在訪問器中執行此操作,以確保調用方無法改變其內部狀態。 您可以在record中添加以下代碼以返回此副本。

public List<String> cities() {
    return List.copyOf(cities);
}

序列化record

如果您的record類實現了可序列化,則可以序列化和反序列化record。不過也有限制。

  1. 可用於替換默認序列化過程的任何系統都不適用於record。創建 writeObject() 和 readObject() 方法不起作用,也不能實現 Externalizable
  2. record可用作代理對象來序列化其他對象。readResolve() 方法可以返回record。也可以在record中添加 writeReplace()。
  3. 反序列化record始終調用標準構造函數。因此,在此構造函數中添加的所有驗證規則都將在反序列化record時強制執行。

這使得record在應用程序中作為數據傳輸對象非常合適。

在實際場景中使用record

record是一個多功能的概念,您可以在許多上下文中使用。

第一種方法是在應用程序的對象模型中攜帶數據。用record充當不可變的數據載體,也是它們的設計目的。

由於可以聲明本地record,因此還可以使用它們來提高代碼的可讀性。

讓我們考慮以下場景。您有兩個建模為record的實體:CityState

public record City(String name, State state) {}
public record State(String name) {}

假設您有一個城市列表,您需要計算擁有最多城市數量的州。可以使用 Stream API 首先使用每個州擁有的城市數構建各州的柱狀圖。此柱狀圖由Map建模。

List<City> cities = List.of();

Map<State, Long> numberOfCitiesPerState =
    cities.stream()
          .collect(Collectors.groupingBy(
                   City::state, Collectors.counting()
          ));

獲取此柱狀圖的最大值是以下代碼。

Map.Entry<State, Long> stateWithTheMostCities =
    numberOfCitiesPerState.entrySet().stream()
                          .max(Map.Entry.comparingByValue())//最多城市
                          .orElseThrow();

最後一段代碼是技術性的;它不具有任何業務意義;因為使用Map.Entry實例對柱狀圖的每個元素進行建模。

使用本地record可以大大改善這種情況。下面的代碼創建一個新的record類,該類包含一個州和該州的城市數。它有一個構造函數,該構造函數將 Map.Entry 的實例作為參數,將鍵值對流映射到record流。

由於需要按城市數比較這些集,因此可以添加工廠方法來提供此比較器。代碼將變為以下內容。

record NumberOfCitiesPerState(State state, long numberOfCities) {

    public NumberOfCitiesPerState(Map.Entry<State, Long> entry) {
        this(entry.getKey(), entry.getValue());//mapping過程
    }

    public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() {
        return Comparator.comparing(NumberOfCitiesPerState::numberOfCities);
    }
}

NumberOfCitiesPerState stateWithTheMostCities =
    numberOfCitiesPerState.entrySet().stream()
                          .map(NumberOfCitiesPerState::new)
                          .max(NumberOfCitiesPerState.comparingByNumberOfCities())//record替換Entry
                          .orElseThrow();

您的代碼現在以有意義的方式提取最大值。您的代碼更具可讀性,更易於理解,不易出錯,從長遠來看更易於維護。

使用collector作為末端操作

讓我們回到Stream API。

使用collector收集流元素

您已經使用了一個非常有用的模式collect(Collectors.toList())來收集由 List 中的流處理的元素。此 collect() 方法是在 Stream 接口中定義的末端方法,它將 Collector 類型的對象作為參數。此Collector接口定義了自己的 API,可用於創建任何類型的內存中結構來存儲流處理的數據。可以在CollectionMap的任何實例中進行收集,它可用來創建字符串,並且您可以創建自己的Collector實例以將自己的結構添加到列表中。

將使用的大多數collector都可以使用 Collectors 工廠類的工廠方法之一創建。這是您在編寫 Collectors.toList() 或 Collectors.toSet()) 時所做的。使用這些方法創建的一些collector可以組合使用,從而產生更多的collector。本教程涵蓋了所有這些要點。

如果在此工廠類中找不到所需的內容,則可以決定通過實現 Collector 接口來創建自己的collector。本教程還介紹瞭如何實現此接口。

Collector API 在 Stream 接口和專用數字流IntStreamLongStreamDoubleStream中的處理方式不同:。Stream 接口有兩個 collect() 方法重載,而數字流只有一個。缺少的正是將collector對象作為參數的那個。因此,不能將collector對象與專用的數字流一起使用。

在集合中收集

Collectors工廠類提供了三種方法,用於在Collection接口的實例中收集流的元素。

  1. toList()) 將它們收集在 List 對象中。
  2. toSet()) 將它們收集在 Set 對象中。
  3. 如果需要任何其他Collection實現,可以使用 toCollection(supplier),)其中 supplier 參數將用於創建所需的 Collection 對象。如果您需要在 LinkedList 實例中收集您的數據,您應該使用此方法。

代碼不應依賴於這些方法當前返回的 ListSet 的確切實現,因為它不是標準的一部分。

您還可以使用 unmodifiableList()) 和 toUnmodifiableSet()) 兩種方法獲取 ListSet 的不可變實現。

以下示例顯示了此模式的實際應用。首先,讓我們在一個普通List實例中收集。

List<Integer> numbers =
IntStream.range(0, 10)
         .boxed()//需要裝箱
         .collect(Collectors.toList());
System.out.println("numbers = " + numbers);

此代碼使用 boxed()) 中繼方法從 IntStream.range() 創建的 IntStream) 創建一個 Stream,方法是對該流的所有元素進行裝箱。運行此代碼將打印以下內容。

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

第二個示例創建一個只有偶數且沒有重複項的 HashSet

Set<Integer> evenNumbers =
IntStream.range(0, 10)
         .map(number -> number / 2)
         .boxed()
        .collect(Collectors.toSet());
System.out.println("evenNumbers = " + evenNumbers);

運行此代碼將產生以下結果。

evenNumbers = [0, 1, 2, 3, 4]

最後一個示例使用 Supplier 對象來創建用於收集流元素的 LinkedList 實例。

LinkedList<Integer> linkedList =
IntStream.range(0, 10)
         .boxed()
         .collect(Collectors.toCollection(LinkedList::new));
System.out.println("linked listS = " + linkedList);

運行此代碼將產生以下結果。

linked list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

使用collector計數

Collectors 工廠類為您提供了幾種方法來創建collector,這些collector執行的操作與普通末端方法為您提供的操作相同。Collectors.counting)() 工廠方法就是這種情況,它與在流上調用 count()) 相同。

這是值得注意的,您可能想知道為什麼使用兩種不同的模式實現了兩次這樣的功能。將在下一節有關在map中收集時回答此問題,您將在其中組合collector以創建更多collector。

目前,編寫以下兩行代碼會導致相同的結果。

Collection<String> strings = List.of("one", "two", "three");

long count = strings.stream().count();
long countWithACollector = strings.stream().collect(Collectors.counting());

System.out.println("count = " + count);
System.out.println("countWithACollector = " + countWithACollector);

運行此代碼將產生以下結果。

count = 3
countWithACollector = 3

收集在字符串中

Collectors 工廠類提供的另一個非常有用的collector是 joining()) 。此collector僅適用於字符串流,並將該流的元素連接為單個字符串。它有幾個重載。

  • 第一個將分隔符作為參數。
  • 第二個將分隔符、前綴和後綴作為參數。

讓我們看看這個collector的實際效果。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining());

System.out.println("joined = " + joined);

運行此代碼將生成以下結果。

joined = 0123456789

可以使用以下代碼向此字符串添加分隔符。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining(", "));

System.out.println("joined = " + joined);

結果如下。

joined = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

讓我們看看最後一個重載,它接收分隔符、前綴和後綴。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining(", ", "{"), "}");

System.out.println("joined = " + joined);

結果如下。

joined = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

請注意,此collector可以正確處理流為空或僅處理單個元素的極端情況。

當您需要生成此類字符串時,此collector非常方便。即使您前面的數據不在集合中或只有幾個元素,您也可能想使用它。如果是這種情況,使用 String.join()) 工廠類或 StringJoiner 對象都將正常工作,無需支付創建流的開銷。

使用Predicate對元素進行分區

Collector API 提供了三種模式,用於從流的元素創建map。我們介紹的第一個使用布爾鍵創建map。它是使用 partitionningBy()) 工廠方法創建的。

流的所有元素都將綁定到布爾值truefalse。map將綁定到每個值的所有元素存儲在列表中。因此,如果將此collector應用於Stream,它將生成具有以下類型的map:Map<Boolean,List<T>>

測試的Predicate應作為參數提供給collector。

下面的示例演示此collector的操作。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Boolean, List<String>> map =
    strings.stream()
           .collect(Collectors.partitioningBy(s -> s.length() > 4));

map.forEach((key, value) -> System.out.println(key + " :: " + value));

運行此代碼將生成以下結果。

false :: [one, two, four, five, six, nine, ten]
true :: [three, seven, eight, eleven, twelve]

此工廠方法具有重載,它將另一個collector作為參數。此collector稱為下游collector。我們將在本教程的下一段中介紹,屆時我們將介紹 groupingBy()) 。

在map中收集並進行分組

我們提供的第二個collector非常重要,因為它允許您創建柱狀圖。

對map中的流元素進行分組

可用於創建柱狀圖的collector是使用 Collectors.groupingBy()) 方法創建的。此方法具有多個重載。

collector將創建map。通過對其應用 Function 實例,為流的每個元素計算一個鍵。此函數作為 groupingBy()) 方法的參數提供。它在Collector API 中稱為分類器 classifier

除了不應該返回 null 之外,此函數沒有任何限制。

此函數可能會為流的多個元素返回相同的鍵。groupingBy()) 支持這一點,並將所有這些元素收集在一個列表中。

因此,如果您正在處理 Stream 並使用 Function<T, K> 作為分類器,則 groupingBy()) 會創建一個 Map<K,List<T>>

讓我們檢查以下示例。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, List<String>> map =
    strings.stream()
           .collect(Collectors.groupingBy(String::length));//返回<Integer, List<String>>

map.forEach((key, value) -> System.out.println(key + " :: " + value));

此示例中使用的分類器是一個函數,用於從該流返回每個字符串的長度。因此,map按字符串長度將字符串分組到列表中。它具有Map<Interger,List<String>>的類型。

運行此代碼將打印以下內容。

3 :: [one, two, six, ten]
4 :: [four, five, nine]
5 :: [three, seven, eight]
6 :: [eleven, twelve]

對分組後的值進行處理

計算數量

groupingBy()) 方法還接受另一個參數,即另一個collector。此collector在Collector API 中稱為下游collector,但它沒有什麼特別的。使它成為下游collector的原因只是,它作為參數傳遞給前一個collector的創建。

此下游collector用於收集由 groupingBy()) 創建的map的值。

在前面的示例中,groupingBy()) 創建了一個map,其值是字符串列表。如果為 groupingBy()) 方法提供下游collector,API 將逐個流式傳輸這些列表,並使用下游collector收集這些流。

假設您將 Collectors.counting()) 作為下游collector傳遞。將計算的內容如下。

[one, two, six, ten]  .stream().collect(Collectors.counting()) -> 4L
[four, five, nine]    .stream().collect(Collectors.counting()) -> 3L
[three, seven, eight] .stream().collect(Collectors.counting()) -> 3L
[eleven, twelve]      .stream().collect(Collectors.counting()) -> 2L

此代碼不是 Java 代碼,因此您無法執行它。它只是在那裏解釋如何使用這個下游collector。

下面將創建的map取決於您提供的下游collector。鍵不會修改,但值可能會。在 Collectors.counting()) 的情況下,值將轉換為 Long。然後,map的類型將變為 Map<Integer,Long>

前面的示例變為以下內容。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Map<Integer, Long> map =
    strings.stream()
           .collect(
               Collectors.groupingBy(
                   String::length, 
                   Collectors.counting()));//List<String>轉為Stream向下傳遞,變成Long

map.forEach((key, value) -> System.out.println(key + " :: " + value));

運行此代碼將打印以下結果。它給出了每個長度的字符串數,這是字符串長度的柱狀圖。

3 :: 4
4 :: 3
5 :: 3
6 :: 2
連接列表的值

您還可以將 Collectors.joining()) collector作為下游collector傳遞,因為此map的值是字符串列表。請記住,此collector只能用於字符串流。這將創建 Map<Integer,String> 的實例。您可以將上一個示例更改為以下內容。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Map<Integer, String> map =
        strings.stream()
                .collect(
                        Collectors.groupingBy(
                                String::length,
                                Collectors.joining(", ")));//變成String
map.forEach((key, value) -> System.out.println(key + " :: " + value));

運行此代碼將生成以下結果。

3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve

控制map的實例

groupingBy()) 方法的最後一個重載將supplier的實例作為參數,以便您控制需要此collector創建的 Map 實例。

您的代碼不應依賴於 groupingBy()) 返回的確切map類型,因為它不是標準的一部分。

使用ToMap在map中收集

Collector API 為您提供了創建map的第二種模式:Collectors.toMap()) 模式。此模式適用於兩個函數,這兩個函數都應用於流的元素。

  1. 第一個稱為密鑰mapper,用於創建密鑰。
  2. 第二個稱為值mapper,用於創建值。

此collector的使用場景與 Collectors.groupingBy()) 不同。特別是,它不處理流的多個元素生成相同密鑰的情況。這種情況下,默認情況下會引發IllegalStateException

這個collector能非常方便的創建緩存。假設User類有一個類型為 LongprimaryKey屬性。您可以使用以下代碼創建User對象的緩存。

List<User> users = ...;

Map<Long, User> userCache = 
    users.stream()
         .collect(User::getPrimaryKey, 
                 Function.idendity());//key必須不同

使用 Function.identity()) 工廠方法只是告訴collector不要轉換流的元素。

如果您希望流的多個元素生成相同的鍵,則可以將進一步的參數傳遞給 toMap()) 方法。此參數的類型為 BinaryOperator。當檢測到衝突元素時,實現將它應用於衝突元素。然後,您的binary operator將生成一個結果,該結果將代替先前的值放入map中。

下面演示如何使用具有衝突值的此collector。此處的值用分隔符連接在一起。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, String> map =
    strings.stream()
            .collect(
                    Collectors.toMap(
                            element -> element.length(),
                            element -> element, 
                            (element1, element2) -> element1 + ", " + element2));//相同key,解決衝突,返回新值

map.forEach((key, value) -> System.out.println(key + " :: " + value));

在此示例中,傳遞給 toMap()) 方法的三個參數如下:

  1. element -> element.length()鍵mapper
  2. element -> element值mapper
  3. (element1, element2) -> element1 + ", " + element2)合併函數,相同鍵的兩個元素會調用。

運行此代碼將生成以下結果。

3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve

另外也可以將supplier作為參數傳遞給 toMap()) 方法,以控制此collector將使用的 Map 接口實例。

toMap()) collector有一個孿生方法 toConcurrentMap(),)它將在併發map中收集數據。實現不保證map的確切類型。

從柱狀圖中提取最大值

groupingBy()) 是分析計算柱狀圖的最佳模式。讓我們研究一個完整的示例,其中您構建柱狀圖,然後嘗試根據要求找到其中的最大值。

提取唯一的最大值

您要分析的柱狀圖如下。它看起來像我們在前面的示例中使用的那個。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
            .collect(
                    Collectors.groupingBy(
                            String::length,
                            Collectors.counting()));

histogram.forEach((key, value) -> System.out.println(key + " :: " + value));

打印此柱狀圖將得到以下結果。

3 :: 4 //期望是4 =>3
4 :: 3
5 :: 3
6 :: 2

從此柱狀圖中提取最大值應得到結果:3 :: 4。Stream API 具有提取最大值所需的所有工具。不幸的是,Map接口上沒有stream()方法。要在map上創建流,您首先需要獲取可以從map獲取的集合之一。

  1. entrySet()) 方法的映射集。
  2. keySet()) 方法的鍵集。
  3. 或者使用 values()) 方法收集值。

這裏你需要鍵和最大值,所以正確的選擇是流式傳輸 entrySet()) 返回的集合。

您需要的代碼如下。

Map.Entry<Integer, Long> maxValue =
    histogram.entrySet().stream()
             .max(Map.Entry.comparingByValue())
             .orElseThrow();

System.out.println("maxValue = " + maxValue);

您可以注意到,此代碼使用 Stream 接口中的 max()) 方法,該方法將comparator作為參數。實際上,Map.Entry 接口的確有幾個工廠方法來創建這樣的comparator。我們在此示例中使用的這個,創建了一個可以比較 Map.Entry 實例的comparator,使用這些鍵值對的值。僅當值實現Comparable接口時,此比較才有效。

這種代碼模式非常普通,只要具有可比較的值,就可以在任何map上使用。我們可以使其特別一點,更具可讀性,這要歸功於Java SE 16中記錄Record的引入。

讓我們創建一個record來模擬此map的鍵值對。創建record只需要一行。由於該語言允許local records,因此您可以到任何方法中。

record NumberOfLength(int length, long number) {
    
    static NumberOfLength fromEntry(Map.Entry<Integer, Long> entry) {
        return new NumberOfLength(entry.getKey(), entry.getValue());//mapping過程
    }

    static Comparator<NumberOfLength> comparingByLength() {
        return Comparator.comparing(NumberOfLength::length);
    }
}

使用此record,以前的模式將變為以下內容。

NumberOfLength maxNumberOfLength =
    histogram.entrySet().stream()
             .map(NumberOfLength::fromEntry)
             .max(NumberOfLength.comparingByLength())//Record替換Entry,後面要引用字段
             .orElseThrow();

System.out.println("maxNumberOfLength = " + maxNumberOfLength);

運行此示例將打印出以下內容。

maxNumberOfLength = NumberOfLength[length=3, number=4]

您可以看到此record看起來像 Map.Entry 接口。它有一個mapping鍵值對的工廠方法和一個用於創建comparator的工廠方法。柱狀圖的分析變得更加可讀和易於理解。

提取多個最大值

前面的示例是一個很好的示例,因為列表中只有一個最大值。不幸的是,現實生活中的情況通常不是那麼好,您可能有幾個與最大值匹配的鍵值對。

讓我們從上一個示例的集合中刪除一個元素。

Collection<String> strings =
    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
            .collect(
                    Collectors.groupingBy(
                            String::length,
                            Collectors.counting()));

histogram.forEach((key, value) -> System.out.println(key + " :: " + value));

打印此柱狀圖將得到以下結果。

3 :: 3
4 :: 3
5 :: 3//期望是3 =>[3,4,5]
6 :: 2

現在我們有三個鍵值對的最大值。如果使用前面的代碼模式提取它,則將選擇並返回這三個中的一個,隱藏其他兩個。

解決此問題的解決方案是創建另一個map,其中鍵是字符串數量,值是與之匹配的長度。換句話説:您需要反轉此map。對於 groupingBy()) 來説,這是一個很好的場景。此示例將在本部分的後面介紹,因為我們還需要一個元素來編寫此代碼。

使用中繼collector

到目前為止,我們介紹的collector只是計數、連接和收集到列表或map中。它們都屬於末端操作。Collector API 也提供了執行中繼操作的其他collector:mapping、filtering和flatmapping。您可能想知道這樣的意義是什麼。事實上,這些特殊的collector並不能單獨創建。它們的工廠方法都需要下游collector作為第二個參數。

也就是説,您這樣創建的整體collector是中繼操作和末端操作的組合。

使用collector來mapping

我們可以檢查的第一個中繼操作是mapping操作。mapping collector是使用 Collectors.mapping()) 工廠方法創建的。它將常規mapping函數作為第一個參數,將必需的下游collector作為第二個參數。

在下面的示例中,我們將mapping與列表中mapping後的元素的集合相結合。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

List<String> result = 
    strings.stream()
        .collect(
            Collectors.mapping(String::toUpperCase, Collectors.toList()));//集成了mapping

System.out.println("result = " + result);

Collectors.mappping()) 工廠方法創建一個常規collector。您可以將此collector作為下游collector傳遞給任何接受collector的方法,例如,包括 groupingBy()) 或 toMap()。)您可能還記得在“提取多個最大值”一節中,我們留下了一個關於反轉map的懸而未決的問題。讓我們使用這個mapping collector來解決問題。

在此示例中,您創建了一個柱狀圖。現在,您需要使用 groupingBy()) 反轉此柱狀圖以查找所有最大值。

以下代碼創建此類map。

Map<Integer, Long> histogram = ...; 

var map = 
    histogram.entrySet().stream()
        .map(NumberOfLength::fromEntry)
        .collect(
            Collectors.groupingBy(NumberOfLength::number));//<Long, List<NumberOfLength>>

讓我們檢查此代碼並確定所構建map的確切類型。

此map的鍵是每個長度在原始流中存在的次數。它是NumberOfLengthrecord的number部分,Long。類型。

這些值是此流的元素,收集到列表中。因此,是NumberOfLength的對象列表。這張map的確切類型是Map<Long,NumberOfLength>

當然,這不是您所要的。您需要的只是字符串的長度,而不是record。從record中提取組件是一個mapping過程。您需要將這些NumberOfLength實例mapping為其length組件。現在我們介紹了mapping collector,可以解決這一點。您需要做的就是將正確的下游collector添加到 groupingBy()) 調用中。

代碼將變為以下內容。

Map<Integer, Long> histogram = ...; 

var map = 
    histogram.entrySet().stream()
        .map(NumberOfLength::fromEntry)
        .collect(
            Collectors.groupingBy(
                NumberOfLength::number, 
                Collectors.mapping(NumberOfLength::length, Collectors.toList())));//<Long, List<Integer>>

構建的map的值現在是使用NumberOfLength::lengthNumberOfLength做mapping後生成的對象列表。此map的類型為Map<Long,List<Integer>>,這正是您所需要的。

要獲取所有最大值,您可以像之前那樣,使用 key 獲取最大值而不是值。

柱狀圖中的完整代碼,包括最大值提取,如下所示。

Map<Long, List<Integer>> map =
    histogram.entrySet().stream()
             .map(NumberOfLength::fromEntry)
             .collect(
                Collectors.groupingBy(
                    NumberOfLength::number,//變成了number=>length列表
                    Collectors.mapping(NumberOfLength::length, Collectors.toList())));

Map.Entry<Long, List<Integer>> result =
    map.entrySet().stream()
       .max(Map.Entry.comparingByKey())//再求key的max
       .orElseThrow();

System.out.println("result = " + result);

運行此代碼將生成以下內容。

result = 3=[3, 4, 5]//最多的length列表

這意味着有三種長度的字符串在此流中出現三次:3、4 和 5。

此示例顯示嵌套在另外兩個collector中的collector,在使用此 API 時,這種情況經常發生。乍一看可能看起來很嚇人,但它只是使用下游collector組合成了collector。

您可以看到為什麼擁有這些中繼collector很有趣。通過使用collector提供的中繼操作,您可以為幾乎任何類型的處理創建下游collector,從而對map的值進行後續處理。

使用collector進行filtering和flatmapping

filtering collector遵循與mapping collector相同的模式。它是使用 Collectors.filtering()) 工廠方法創建的,該方法接收常規Predicate來filter數據,同時要有必需的下游collector。

Collectors.flatMapping()) 工廠方法創建的flatmapping collector也是如此,它接收flatmapping函數(返回流的函數)和必需的下游collector。

使用末端collector

Collector API 還提供了幾個末端操作,對應於Stream API 上可用的末端操作。

  • maxBy() 和) minBy()。)這兩個方法都將comparator作為參數,如果處理的流本身為空,則返回一個Optional對象。
  • summingInt)()、summingLong()) 和 summingDouble()。)這三種方法將mapping函數作為參數,分別將流的元素mapping為intlongdouble ,然後對它們求和。
  • averageagingInt())、 averageagingLong())和averageagingDouble()).這三種方法也將mapping函數作為參數,在計算平均值之前分別將流的元素map為 intlongdouble。這些collector的工作方式與 IntStreamLongStreamDoubleStream 中定義的相應 average()) 方法不同。它們都返回一個 Double 實例,對於空流返回 0。而數字流的average()) 方法返回一個Optional對象,該對象對於空流為空。

創建自己的collector

瞭解collector的工作原理

如前所述,Collectors工廠類僅處理對象流,因為將collector對象作為參數的 collect() 方法僅存在於 Stream 中。如果您需要收集數字流,那麼您需要了解collector的組成元素是什麼。

簡單説,collector建立在四個基本組件之上。前兩個用於收集流的元素。第三個僅用於並行流。某些類型的collector需要第四個,這些collector需要對構建的容器作後續處理。

第一個組件用於創建收集流元素的容器。此容器易於識別。例如,在上一部分介紹的情況下,我們使用了 ArrayList 類、HashSet 類和 HashMap 類。可以使用supplier實例對創建此類容器進行建模。第一個組件稱為supplier

第二個組件旨在將流中的單個元素添加到容器。Stream API 的實現將重複調用此操作,將流的所有元素逐個添加到容器中。

在Collector API中,此組件由BiConsumer的實例建模。這個biconsumer有兩個參數。

  1. 第一個是容器本身,流的先前元素填充了部分。
  2. 第二個是應添加的流元素。

此biconsumer在Collector API 的上下文中稱為accumulator

這兩個組件應該足以讓collector工作,但 Stream API 帶來了一個約束,使collector正常工作需要另外兩個組件。

你可能還記得,Stream API 支持並行化。本教程稍後將更詳細地介紹這一點。您需要知道的是,並行化將流的元素拆分為子流,每個元素都由 CPU 的內核處理。Collector API 可以在這樣的上下文中工作:每個子流將只收集在自己的容器實例中。

處理完這些子流後,您將擁有多個容器,每個容器都包含它所處理的子流中的元素。這些容器是相同的,因為它們是與同一supplier一起創建的。現在,您需要一種方法將它們合併為一個。為了能夠做到這一點,Collector API 需要第三個組件,即combiner,它將這些容器合併在一起。combiner由 BinaryOperator 的實例建模,該實例接收兩個部分填充的容器並返回一個。

Stream API 的 collect() 也有個重載,這個 BinaryOperator 變成了 BiConsumer ,我們主要使用這個。

第四個組件稱為finisher,本部分稍後將介紹。

在集合中收集原始類型

使用前三個組件,您可以嘗試專用數字流中的 collect()) 方法。IntStream.collect()) 方法有三個參數:

  • Supplier的實例,稱為supplier;
  • ObjIntConsumer的實例,稱為accumulator;
  • BiConsumer的實例,稱為combiner

讓我們編寫代碼以在List<Integer>中收集IntStream

Supplier<List<Integer>> supplier                  = ArrayList::new;//容器
ObjIntConsumer<List<Integer>> accumulator         = Collection::add;//元素如何進入容器
BiConsumer<List<Integer>, List<Integer>> combiner = Collection::addAll;//多個片段如何合併

List<Integer> collect =
    IntStream.range(0, 10)
             .collect(supplier, accumulator, combiner );

System.out.println("collect = " + collect);

運行此代碼將生成以下結果。

collect = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

將這些數據收集為Set只需要更改supplier的實現並相應地調整類型。

在 StringBuffer 中收集原始類型

讓我們研究一下如何自己實現 Collectors.joining()) ,以將原始類型流的元素連接在單個字符串中。String 類是不可變的,因此無法在其中累積元素。您可以使用可變的 StringBuffer 類。

StringBuffer 中收集元素遵循與前一個相同的模式。

Supplier<StringBuffer> supplier                 = StringBuffer::new;//
ObjIntConsumer<StringBuffer> accumulator        = StringBuffer::append;
BiConsumer<StringBuffer, StringBuffer> combiner = StringBuffer::append;

StringBuffer collect = 
    IntStream.range(0, 10)
             .collect(supplier, accumulator, combiner);

System.out.println("collect = " + collect);

運行此代碼將生成以下結果。

collect = 0123456789

使用finisher對collector進行後續處理

你在上一段中編寫的代碼幾乎完成了你需要的:它在 StringBuffer 實例中連接字符串,你可以通過調用它的 toString()) 方法來創建一個常規的 String 對象。但是 Collectors.joining()) collector直接生成一個字符串,而無需你調用 toString()。)那麼它是怎麼做到的呢?

Collector API 精確地定義了第四個組件來處理這種情況,稱為finisher。finisher是一個Function,它獲取累積元素的容器並將其轉換為其他內容。在 Collectors.joining()) 的情況下,這個函數只是下面的。

Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();

對很多collector來説,finisher只是恆等函數。比如:toList())、toSet)()、groupingBy()) 和 toMap()。)

其他情況下,collector內部使用的可變容器成為中繼容器,在返回到應用程序之前,該容器將mapping為其他對象(可能是另一個容器)。這就是Collector API 處理不可變列表、set或map創建的方式。finisher用於將中繼容器密封到不可變容器中,返回到應用程序。

finisher還有其他用途,可以提高代碼的可讀性。Collectors 工廠類有一個工廠方法,我們還沒有介紹:collectingAndThen()) 方法。此方法將collector作為第一個參數,將finisher作為第二個參數。它會將第一個collector收集的結果,使用您提供的finisher對其進行mapping。

您可能還記得以下示例,我們已經在前面的部分中多次檢查過該示例。它是關於提取柱狀圖的最大值。

Collection<String> strings =
    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
           .collect(
                   Collectors.groupingBy(
                           String::length,
                           Collectors.counting()));

Map.Entry<Integer, Long> maxValue =
    histogram.entrySet().stream()
             .max(Map.Entry.comparingByValue())
             .orElseThrow();

System.out.println("maxValue = " + maxValue);

第一步,您構建了 Map<Integer,Long> 類型的柱狀圖,在第二步中,您提取了此柱狀圖的最大值,按值比較鍵值對。

第二步實際上是將map轉換為特殊的鍵/值對。您可以使用以下函數對其進行建模。

Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher = 
    map -> map.entrySet().stream()
              .max(Map.Entry.comparingByValue())
              .orElseThrow();

此函數的類型起初可能看起來很複雜。事實上,它只是從map中提取一個鍵值對,類型為 Map.Entry

現在您已經有了這個函數,您可以使用 collectingAndThen()) 將此最大值提取步驟集成到collector本身中。然後,模式將變為以下內容。

Collection<String> strings =
        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher =
    map -> map.entrySet().stream()
              .max(Map.Entry.comparingByValue())
              .orElseThrow();//提取此finisher需要特別注意類型,消費Map,產出Entry

Map.Entry<Integer, Long> maxValue =
    strings.stream()
           .collect(
               Collectors.collectingAndThen(
                   Collectors.groupingBy(
                           String::length,
                           Collectors.counting()),
                   finisher
               ));

System.out.println("maxValue = " + maxValue);

您可能想知道為什麼需要編寫此看起來非常複雜的代碼?

現在,您已經擁有了由單個collector建模的最大值提取器,您可以將其用作另一個collector的下游collector。做到這一點,可以組合更多的collector對您的數據進行更復雜的計算。

將兩個collector的結果與三通collector相結合

在 Java SE 12 的 Collectors 類中添加了一個名為 teeing()) 的方法。此方法需要兩個下游collector和一個合併函數。

讓我們通過一個場景,看看您可以使用collector做什麼。想象一下,您有以下CarTruck兩種record。

enum Color {
    RED, BLUE, WHITE, YELLOW
}

enum Engine {
    ELECTRIC, HYBRID, GAS
}

enum Drive {
    WD2, WD4
}

interface Vehicle {}

record Car(Color color, Engine engine, Drive drive, int passengers) {}

record Truck(Engine engine, Drive drive, int weight) {}

Car對象有幾個組成部分:顏色、引擎、驅動器以及它可以運輸的一定數量的乘客。Truck有引擎,有驅動器,可以運輸一定量的貨物。兩者都實現相同的接口:Vehicle

假設您有一系列Vehicle,您需要找到所有配備電動引擎的Car。根據您的應用程序,您可能會使用流filter您的Car集合。或者,如果您知道下一個需求,將是找到配備混合動力引擎的Car,您可能更願意準備一個map,以引擎為鍵,並以配備該引擎的Car列表作為值。在這兩種情況API 都會為你提供正確的模式來獲取所需的內容。

假設您需要將所有電動Truck添加到此集合中。也有可能想一次處理所有Vehicle,但是用於filter數據的Predicate變得越來越複雜。它可能如下所示。

Predicate<Vehicle> predicate =
    vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC ||
               vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;
//這個是instanceof新用法,後面直接賦值變量,同時跟短路操作

您真正需要的是以下內容:

  1. filterVehicle以獲得所有電動Car
  2. filterVehicle以獲得所有電動Truck
  3. 合併兩個結果。

這正是teeing collector可以為您做的事情。teeing collector由 Collectors.teeing()) 工廠方法創建,該方法接收三個參數。

  1. 第一個下游collector,用於收集流的數據。
  2. 第二個下游collector,也用於收集數據。
  3. 一個bifunction,用於合併由兩個下游collector創建的兩個容器。

您的數據將一次性處理,以保證最佳性能。

我們已經介紹了使用collector來filter流元素的模式。合併函數只是對 Collection.addAll()) 方法的調用。以下是代碼:

List<Vehicle> electricVehicles = vehicles.stream()
    .collect(
        Collectors.teeing(
            Collectors.filtering(
                vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC,
                Collectors.toList()),
            Collectors.filtering(
                vehicle -> vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC,
                Collectors.toList()),
            (cars, trucks) -> {
                cars.addAll(trucks);
                return cars;
            }));

實現collector接口

為什麼要實現collector接口?

有三種方法可以創建自己的collector。

包括將現有collector與Collectors工廠類結合,將collector作為下游collector傳遞給另一個collector,或者作為finisher一起使用 collectingAndThen()) 。我們上面教程中已經介紹過。

您還可以調用 collect()) 方法,該方法接收構建collector的三個元素。這些方法在原始類型流和對象流上都可用。他們接收了我們在前面部分中提出的三個參數。

  1. 用於創建可變容器的supplier,其中累積了流的元素。
  2. accumulator,由biconsumer建模。
  3. combiner也由biconsumer建模,用於組合兩個部分填充的容器,用於並行流的情況。

第三種方法是自己實現 Collector 接口,並將您的實現傳遞給我們已經介紹過的 collect() 方法。實現自己的collector可以為您提供最大的靈活性,但也更具技術性。

瞭解collector的參數類型

讓我們檢查一下這個接口的參數。

interface Collector<T, A, R> {
    
    // content of the interface
}

讓我們首先檢查以下類型:T,R

第一種類型是 ,它對應於此collector正在處理的流元素的類型。T

最後一個類型是 ,它是此collector生成的類型。R

比如在 Stream 實例上調用的 toList()) collector,類型RList。它 toSet()) collector將是 Set

groupingBy()) 方法接收一個函數作參數,來計算返回map的鍵。如果用它收集 Stream,則需要傳遞一個對T實例作mapping 的函數。因此,生成的map的類型將為 Map<K,List<T>>。也就是R的類型。

A類型處理起來比較複雜。您可能已嘗試使用 IDE 來存儲您在前面的示例中創建的collector之一。如果這樣做,您可能意識到 IDE 沒有為此類型提供顯式值。以下示例就是這種情況。

Collection<String> strings =
        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Collector<String, ?, List<String>> listCollector = Collectors.toList();
List<String> list = strings.stream().collect(listCollector);

Collector<String, ?, Set<String>> setCollector = Collectors.toSet();
Set<String> set = strings.stream().collect(setCollector);

Collector<String, ?, Map<Integer, Long>> groupingBy = 
        Collectors.groupingBy(String::length, Collectors.counting());
Map<Integer, Long> map = strings.stream().collect(groupingBy);

對於所有這些collector,第二個參數類型僅為 ?

如果需要實現Collector接口,則必須為A提供個顯式值。A是此collector使用的中繼可變容器的實際類型。對於 toList()) collector,它將是 ArrayList,對於 toSet()) collector,它將是 HashSet。事實上,此類型被 toList()) 的返回類型隱藏了,這就是為什麼在前面的示例中無法將 ?類型替換為 ArrayList 的原因。

即使內部可變容器是由實現直接返回的,也可能發生類型AR不同的情況。例如, toList()) ,您可以通過修改 ArrayList<T> 和 List<T> 來實現 Collector><T,A,R> 接口。

瞭解collector的特徵

collector定義了內部特徵,流實現用它來優化collector使用。

有三個。

  1. IDENTITY_FINISH指示此collector的finisher是恆等函數。該實現不會為具有此特徵的collector調用finisher。
  2. UNORDERED指示此collector不保留它處理流元素的順序。toSet()) collector就是這種情況。而toList()) 就沒有。
  3. CONCURRENT 特性表示accumulator用來存儲已處理元素的容器支持併發訪問。這一點對於並行流很重要。

這些特徵在collectorCollector.Characteristics枚舉中定義,並由Collector接口的 characteristics()) 方法以set返回。

實現 toList() 和 toSet() collector

使用這些元素,您現在可以重新創建類似於 toList()) collector的實現。

class ToList<T> implements Collector<T, List<T>, List<T>> {


    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return Collection::add;
    }

    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {list1.addAll(list2); return list1; };
    }

    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.IDENTITY_FINISH);//不調用finisher
    }
}

可以使用以下模式使用此collector。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five") ;

List<String> result = strings.stream().collect(new ToList<>());
System.out.println("result = " + result);

此代碼打印出以下結果。

result = [one, two, three, four, five]

實現一個類似於 toSet()) 的collector只需要兩處修改。

  • supplier()) 方法將返回 HashSet::new
  • characteristics()) 方法會將 Features.UNORDERED 添加到返回的set中。

實現 joining() collector

重新創建此collector的實現很有趣,因為它只對字符串進行操作,並且它的finisher不是恆等函數。

此collector在 StringBuffer 實例中累積它處理的字符串,然後調用 toString()) 方法以生成final結果。

此collector的特徵集為空。它確實保留了處理元素的順序(因此沒有UNORDERED特徵),它的finisher不是恆等函數,並且不能併發使用。

讓我們看看如何實現這個collector。

class Joining implements Collector<String, StringBuffer, String> {

    public Supplier<StringBuffer> supplier() {
        return StringBuffer::new;
    }

    public BiConsumer<StringBuffer, String> accumulator() {
        return StringBuffer::append;
    }

    public BinaryOperator<StringBuffer> combiner() {
        return StringBuffer::append;
    }

    public Function<StringBuffer, String> finisher() {//會調用
        return Object::toString;
    }

    public Set<Characteristics> characteristics() {
        return Set.of();
    }
}

您可以在以下示例中看到如何使用此collector。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five") ;

String result = strings.stream().collect(new Joining());
System.out.println("result = " + result);

運行此代碼將生成以下結果。

result = onetwothreefourfive

要支持分隔符、前綴和後綴可以使用 StringJoiner

使用Optional

創建Optional對象

Optional 類是具有私有構造函數的final類。因此,創建它實例的唯一方法是調用其工廠方法之一。其中有三個。

  1. 您可以通過調用 Optional.empty()) 創建一個空的Optional。
  2. 您可以通過調用 Optional.of()) 將某元素作為參數。不允許將 null傳遞給此方法。這種情況下,您將獲得一個 NullPointerException
  3. 您可以通過調用 Optional.ofNullable()) 將某元素作為參數。可以將null傳遞給此方法。這種情況下,您將獲得一個空的Optional。

這些是創建此類實例的唯一方法。如您所見,不能將null直接賦給Optional對象。打開非空Optional將始終返回非null。

Optional<T> 有三個等效的類,用於專用數字流:OptionalIntOptionalLongOptionalDouble。這些類是原始類型(即值)的包裝器。ofNullable()) 方法對這些類沒有意義,因為原始值不能為null。

打開Optional對象

有幾種方法可以使用Optional元素並訪問它包裝的元素(如果有)。你可以直接查詢你擁有的實例,如果裏面有東西,就打開它,或者你可以在上面使用類似流的方法: map()), flatMap()), filter()),甚至是 forEach()) 的等價物。

打開Optional以獲取其內容時應謹慎,因為如果Optional為空,它將引發 NoSuchElementException。除非您確定Optional元素中存在元素,否則應首先通過測試來保護此操作。

有兩種方法可供您測試Optional對象:isPresent()) 和 isEmpty(),)它們在 Java SE 11 中添加。

然後,要打開您的Optional,您可以使用以下方法。

  • get()):此方法已被棄用,因為 is 看起來像一個 getter,但如果Optional為空,它可以拋出 NoSuchElementException
  • orElseThrow()) 是自 Java SE 10 以來的首選模式。它與get())方法相同,但它的名稱毫無疑問它可以拋出NoSuchElementException
  • orElseThrow(Supplier exceptionSupplier):)與前面的方法相同。它使用您傳遞的supplier作為參數來創建它引發的異常。

您還可以提供一個對象,如果Optional對象為空,將返回該對象。

  • orElse(T returnObject):)如果在空的Optional值上調用,則返回參數。
  • orElseGet(Supplier supplier):)與前一個相同。實際上,僅在需要時調用所提供的supplier。

最後,如果此Optional為空,則可以創建另一個Optional。

  • or(supplier<Optional>supplier):)如果它不為空,則返回此未修改的Optional,如果空,則調用提供的supplier。此supplier創建另一個Optional供方法返回。

處理Optional對象

Optional 類還提供模式,以便您可以將Optional對象與流處理集成。它具有直接對應Stream API 的方法,您可以使用這些方法以相同的方式處理數據,並且將與流無縫集成。這些方法是 map()), filter()),和flatMap()),前兩個接收的參數與Stream API中的方法相同,後者的函數參數需要返回Optional<T>而不是Stream

這些方法按以下規則返回Optional對象。

  1. 如果調用的對象為空,則返回Optional。
  2. 如果不為空,則它們的參數、函數或Predicate將應用於此Optional的內容。將結果包裝在另一個Optional中返回。

使用這些方法可以在某些流模式中生成更具可讀性的代碼。

假設您有一個具有id屬性的Customer實例列表。您需要查找具有給定 ID 的客户的名稱。

您可以使用以下模式執行此操作。

String findCustomerNameById(int id){
    List<Customer> customers = ...;

    return customers.stream()
                    .filter(customer->customer.getId() == id);
                    .findFirst()//返回Optional
                    .map(Customer::getName)
                    .orElse("UNKNOWN");
}

您可以看到 map()) 方法來自 Optional 類,它與流處理很好地集成在一起。你不需要檢查 findFirst()) 方法返回的Optional對象是否為空;調用map())實際上可以為您執行此操作。

找出發表文章最多的兩位聯合作者

讓我們看另一個更復雜的示例。通過此示例,向您展示Stream API、Collector API 和Optional對象的幾種主要模式。

假設您有一組需要處理的文章。一篇文章有標題、發表年份和作者列表。作者有一個名字。

您的列表中有很多文章,您需要知道哪些作者一起聯合發表了最多的文章。

您的第一個想法可能是為文章構建一對作者的流。這實際上是文章和作者集的笛卡爾乘積。您並不需要此流中的所有對。您對兩位作者實際上是同一對的情況不感興趣;一對作者(A1,A2)與(A2A1)實際相同。若要實現此約束,可以添加約束條件,聲明一對作者時,聲明作者按字母順序排序。

讓我們為這個模型寫兩條record。

record Article (String title, int inceptionYear, List<Author> authors) {}

record Author(String name) implements Comparable<Author> {

    public int compareTo(Author other) {
        return this.name.compareTo(other.name);
    }
}

record PairOfAuthors(Author first, Author second) {
    
    public static Optional<PairOfAuthors> of(Author first, Author second) {//用Optional實現了排序後的創建
        if (first.compareTo(second) > 0) {
            return Optional.of(new PairOfAuthors(first, second));
        } else {
            return Optional.empty();
        }
    }
}

PairOfAuthorsrecord中的創建工廠方法,可以控制哪些實例是允許的,並防止不需要的創建。若要表明此工廠方法可能無法生成結果,可以將其包裝在Optional方法中。這完全尊重了以下原則:如果無法生成結果,則返回一個空的 optional。

讓我們編寫一個函數,為給定的文章創建一個 Stream<PairOfAuthors> 。您可以用兩個嵌套流生成笛卡爾乘積。

作為第一步,您可以編寫一個bifunction,從文章和作者創建此流。

BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors =
    (article, firstAuthor) ->
        article.authors().stream().flatMap(//對每個author都遍歷authors創建作者對,生成Stream<PairOfAuthors>
            secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());//Optional的Stream

此bifunction從 firstAuthorsecondAuthor 創建一個Optional對象,取自基於文章作者構建的流。您可以看到 stream()) 方法是在 of()) 方法返回的Optional對象上調用的。如果Optional流為空,則返回的流為空,否則僅包含一對作者。此流由 flatMap()) 方法處理。此方法打開流,空的流將消失,並且只有有效的對將出現在生成的流中。

您現在可以構建一個函數,該函數使用此bifunction從文章中創建作者對流。

Function<Article, Stream<PairOfAuthors>> toPairOfAuthors =
    article ->
    article.authors().stream()
                     .flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));

找到聯合發表最多的兩位作者可以通過柱狀圖來完成,柱狀圖中的鍵是作者對,值是他們一起寫的文章數。

您可以使用 groupingBy()) 構建柱狀圖。讓我們首先創建一對作者的流。

Stream<PairOfAuthors> pairsOfAuthors =
    articles.stream()
            .flatMap(toPairOfAuthors);

此流的構建方式是,如果一對作者一起寫了兩篇文章,則這對作者在流中出現兩次。因此,您需要做的是計算每個對在此流中出現的次數。這可以通過 groupingBy()) 來完成,其中分類器是恆等函數:對本身。此時,這些值是您需要計數的對列表。所以下游collector只是 counting()) collector。

Map<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)//所有文章的Stream<PairOfAuthors>
            .collect(Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()//<PairOfAuthors, Long>
            ));

找到一起發表文章最多的作者包括提取此map的最大值。您可以為此處理創建以下函數。

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor =
    map -> map.entrySet().stream()
                         .max(Map.Entry.comparingByValue())
                         .orElseThrow();

此函數在 Stream.max()) 方法返回的Optional對象上調用 orElseThrow()) 方法。

這個Optional對象可以為空嗎?要使其為空,map本身必須為空,這意味着原始流中沒有成對的作者。只要您至少有一篇文章至少有兩位作者,那麼這個Optional就不為空。

找出每年發表文章最多的兩位聯合作者

讓我們更進一步,想知道您是否可以根據年份進行相同的處理。事實上,如果能使用單個collector實現,接下來就可以將其作為下游collector傳遞給 groupingBy(Article::inceptionYear)) 。

對map後續提取最大值可以使用collectingAndThen())。此模式已在上一節“使用finisher對collector進行後續處理”中介紹過。此collector如下。

讓我們提取 groupingBy()) collector和finisher。如果使用 IDE 鍵入此代碼,可以獲取collector的正確類型。

Collector<PairOfAuthors, ?, Map<PairOfAuthors, Long>> groupingBy =
        Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
        );

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> finisher =
        map -> map.entrySet().stream()
                  .max(Map.Entry.comparingByValue())
                  .orElseThrow();

現在,您可以將它們合併到單個 collectingAndThen()) 中。將 groupingBy()) 作為為第一個參數,將finisher作為第二個。

Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector =
    Collectors.collectingAndThen(
            Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
            ),
            map -> map.entrySet().stream()
                      .max(Map.Entry.comparingByValue())
                      .orElseThrow()
    );

現在,您可以使用初始flatmap操作和此collector編寫完整模式。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)
            .collect(pairOfAuthorsEntryCollector);

多虧了 flatMapping)(),您可以通過合併中繼 flatMap()) 和末端collector來使用單個collector編寫此代碼。以下代碼等效於上一個代碼。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .collect(
                Collectors.flatMapping(
                    toPairOfAuthors,
                    pairOfAuthorsEntryCollector));

找到每年發表最多的兩位聯合作者,只需將這個 flatMapping)() 作為下游collector傳遞給正確的 groupingBy()) 即可。

Collector<Article, ?, Map.Entry<PairOfAuthors, Long>> flatMapping = 
    Collectors.flatMapping(
            toPairOfAuthors,
            pairOfAuthorsEntryCollector));

Map<Integer, Map.Entry<PairOfAuthors, Long>> result =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                    Article::inceptionYear,
                    flatMapping
                )
            );

你可能還記得,在這個flatMapping)()的深處,有一個對Optional.orElseThrow())的調用。在這個的模式中,很容易檢查此調用是否會失敗,因為此時有一個空的Optional很容易猜到。

現在我們已將此collector用作下游collector,情況就不同了。你怎麼能確定,每年至少有一篇文章由至少兩位作者撰寫?保護此代碼免受任何 NoSuchElementException 的影響會更安全。

避免打開Optional

在第一個上下文中可以接受的模式現在更加危險。處理它包括首先不要調用orElseThrow()。)

這種情況下,collector將變為以下項。它不是創建一對作者和一長串數字的鍵值對,而是將結果包裝在一個Optional對象中。

Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>> 
        pairOfAuthorsEntryCollector =
            Collectors.collectingAndThen(
                Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()
                ),
                map -> map.entrySet().stream()
                          .max(Map.Entry.comparingByValue())
            );

請注意,orElseThrow()) 不再被調用,從而導致collector的簽名中有一個Optional。

這個Optional也出現在 flatMapping()) collector的簽名中。

Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =
        Collectors.flatMapping(
                toPairOfAuthors,
                pairOfAuthorsEntryCollector
        );

使用此collector會創建一個類型為 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>> 類型的map,我們不需要這種類型:擁有一個值為Optional的map是無用的,而且可能很昂貴。這是一種反模式。不幸的是,在計算此最大值之前,您無法猜測此Optional是否為空。

構建此中繼map後,您需要刪除空的Optional來構建表示所需柱狀圖的map。我們將使用與之前相同的技術:在flatMap()) 中調用Optional的stream()))方法,以便 flatMap()) 操作靜默刪除空的Optional。

模式如下。

Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                        Article::inceptionYear,
                        flatMapping
                )
            )  // Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>
            .entrySet().stream()
            .flatMap(
                entry -> entry.getValue()//如果Optional為空,會成為空流,從而安全跳過
                              .map(value -> Map.entry(entry.getKey(), value))
                              .stream())
            .collect(Collectors.toMap(
                    Map.Entry::getKey, Map.Entry::getValue
            )); // Map<Integer, Map.Entry<PairOfAuthors, Long>>

請注意此模式中的flatmap函數。它接受一個entry作參數 ,類型為 Optional<Map.Entry<PairOfAuthors, Long>> ,並在此Optional上調用 map()。)

如果Optional為空,則此調用返回空的Optional。然後忽略map函數。接下來調用 stream()) 返回一個空流,該流將從主流中刪除,因為我們處於 flatMap()) 調用中。

如果Optional中有一個值,則使用此值調用map函數。此map函數創建一個具有相同鍵和此現有值的新鍵值對。此鍵值對的類型為 Map.Entry,並且通過此 map()) 方法將其包裝在Optional對象中。對 stream()) 的調用會創建一個包含此Optional內容的流,然後由 flatMap()) 調用打開該流。

此模式用空的Optional將 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>>mapping為 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>,刪除所有具有空Optional的鍵/值對。

使用 toMap()) collector可以安全地重新創建map,因為您知道在此流中不能使用兩次相同的鍵。

此模式使用了Optional和Stream API 的三個要點。

  1. Optional.map()) 方法,如果在空Optional上調用,則返回空的Optional。
  2. Optional.stream()) 方法,該方法在 Optional的內容上打開流。如果Optional為空,則返回的流也為空。它允許您從Optional空間無縫的移動到流空間。
  3. Stream.flatMap()) 方法,用於打開從Optional構建的流,以靜默方式刪除空流。

消費Optional的內容

Optional 類還具有兩個將Consumer作為參數的方法。

  • ifPresent(Consumer consumer)):此方法使用此Optional的內容(如果有)調用提供的Consumer。它實際上等同於 Stream.forEach(Consumer)) 方法。
  • ifPresentOrElse(Consumer consumer, Runnable runnable):)如果 Optional非空,此方法與前一個方法相同。如果空,則調用提供的 Runnable 實例。

燒哥總結

(驗證中,代碼庫持續更新)

lambda將匿名類換成了匿名方法,能代表某個操作,讓代碼更直觀(語法糖),但良好的命名很重要。

改寫為lambda首先得是函數接口,Operator是Function的簡化版。

可以序列化,從而可以作為字段、方法參數和返回類型,實現了方法引用、鏈式調用、函數式編程。

lambda已經深入JDK內部,所以性能方面很關注,為避免裝箱拆箱,提供了很多原生類型專用版,但有時候要手動裝箱。

為了性能,避免在內存中處理大量數據,同時也提高可讀性,出現了Stream API。

流處理的整個過程最好都是流,所以有flatmap、mapMulti各種中繼操作,

甚至末端collector也可以有下游collector,甚至collector可以串聯、三通,比如神奇的Collectors.flatMapping()

流不應該作為變量或參數。

流中不應該改變外圍變量,會捕獲外界變量,降低處理性能,也會把並行流變成多線程併發。

每次中繼操作都產生一個新流。

同樣為了性能,reduce可以並行,但要具有可結合性、要有幺元。如果幺元未知,會返回Optional。

三參數的reduce組合了mapping過程。

專用數字流的sum、min、max、count、average、summaryStatistics為末端操作。

轉換為流的源如果用Set,會是亂序的。

map、flatmap會刪除SORTED、DISTINCTED、NONNULL。

本教程未詳細説明的:spliterator、不可變流、併發流。

Stream.collect(Collectors.toList()) 只能用於對象流,數字流要麼裝箱,要麼用三參數那個,或者自定義collector,五個參數。

flatmap會跳過空流,包括Optional.stream()產生的流,所以看到Optional,不要orElseThrow(),可以用flatmap取出。

API是看起來越來越複雜,Collectors.mapping()

public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper,
 Collector<? super U,A,R> downstream)

方法名前面四個,返回類型裏三個,還有問號,參數裏super了三個。

Map<City, Set<String>> lastNamesByCity
   = people.stream().collect(
     groupingBy(Person::getCity,
                mapping(Person::getLastName,
                        toSet())));

雖然很好用。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.