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

来源:https://www.cnblogs.com/burn-red/archive/2023/07/05/17529799.html
-Advertisement-
Play Games

# Stream API > Stream API 是按照map/filter/reduce方法處理記憶體中數據的最佳工具。 > 本系列教程由Record講起,然後結合Optional,討論collector的設計。 ![](https://i.hongkj.cn/java17/logo-stream ...


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 上可用的末端操作。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要: GoEasy帶來了一項令開發者振奮的消息:全面支持Android原生平臺!現在,您可以在Android應用中使用最酷炫的實時通信功能,藉助GoEasy輕鬆實現消息的發送和接收。本文將帶您領略GoEasy最新版本的威力,為您的應用增添一抹鮮活的互動色彩。 嗨,開發者朋友們!是時候展現您的技術才 ...
  • 對安裝的apk進行校驗,除了系統應用市場中下載的,其它渠道的apk都進行安裝攔截,並且彈框提示。 首先需要把驗證的證書保存在資料庫本地,後面需要用到 然後註冊系統廣播,用於接收 apk 安裝時的監聽,這個廣播由系統發出 新裝時的 action ‘android.intent.action.PACKA ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 遇到的問題 在一個新項目中,設計統一了項目中所有的字體,並提供了字體包。在項目中需要按需引入這些字體包。 首先,字體包的使用分為了以下幾種情況: 無特殊要求的語言使用字體A,阿拉伯語言使用字體B; 加粗、中等、常規、偏細四種樣式,AB兩種 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在使用 Vue 3 組件庫 Naive UI 的數據表格組件 DataTable 時碰到的問題,NaiveUI 的數據表格組件 DataTable 在固定頭部和列的示例中,在鍵盤操作下表格橫向滾動會有問題,本文是記錄下解決問題的過程 ...
  • 1、找到config.json,在配置文件中新增水印效果 /* 上傳圖片配置項 */ "imageWater": "true",/*******************新增圖片水印設置 這裡是新增*/ "imageActionName": "uploadsimage", /* 執行上傳圖片的acti ...
  • # jira安裝具體步驟 1. 安裝docker ![image](https://img2023.cnblogs.com/blog/2627104/202307/2627104-20230705230931019-1424539379.png) 2. 啟動docker ![image](https ...
  • Git是目前IT行業使用率最高的版本控制系統,相信大家在日常工作中也經常使用,每次Git提交都會包含提交信息,常用的包括說明、提交人和提交時間等,此篇文章主要向大家介紹下如何修改這些信息,這些命令在正常使用時可能不常用,但還是建議收藏以備不時之需。 ## 新提交 ### 指定提交信息 在使用`git ...
  • # Java 方法的重載、可變參數、作用域 # 1. 方法的重載 ## 使用相同的方法名來定義不同的方法,方法的重載能優化代碼,減少冗餘度。 ## 在使用方法的重載需要註意的地方有: > ## 1. 方法的重載需要方法名相同,並且形參類別、個數、順序不同(滿足其中之一) > > ## 2. 方法的重 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...