# 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) {}
這一行代碼為您創建以下元素。
- 它是一個不可變的類,有兩個欄位:
x
和y
- 它有一個標準的構造函數,用於初始化這兩個欄位。
toString
()、equals
() 和hashCode()
方法是由編譯器為您創建的,其預設行為與 IDE 將生成的內容相對應。如果需要,可以通過添加自己的實現來修改此行為。- 它可以實現
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中:
- 額外聲明的實例欄位。不能添加任何與組件不對應的實例欄位。
- 實例欄位的初始化。
- 實例的初始化塊。
您可以使用靜態欄位,靜態初始化塊。
使用標準構造函數構造record
編譯器還會為您創建一個構造函數,稱為標準構造函數 canonical constructor。此構造函數以record的組件作為參數,並將其值複製到欄位中。
在某些情況下,您需要覆蓋此預設行為。讓我們研究兩種情況:
- 您需要驗證組件的狀態
- 您需要製作可變組件的副本。
使用緊湊構造函數
可以使用兩種不同的語法來重新定義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()
必須是構造函數的第一個語句。
讓我們檢查以下State
record。它由三個組件定義:
- 此州的名稱
- 該州首府的名稱
- 城市名稱列表,可能為空。
我們需要存儲城市列表的副本,確保它不會從此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的每個組件都有一個訪問器方法,該方法具有此組件的名稱。
但是,某些情況下,您需要定義自己的訪問器。 例如,假設上一節中的State
record在構造期間沒有創建列表的不可修改的副本 - 那麼它應該在訪問器中執行此操作,以確保調用方無法改變其內部狀態。 您可以在record中添加以下代碼以返回此副本。
public List<String> cities() {
return List.copyOf(cities);
}
序列化record
如果您的record類實現了可序列化,則可以序列化和反序列化
record。不過也有限制。
- 可用於替換預設序列化過程的任何系統都不適用於record。創建 writeObject() 和 readObject() 方法不起作用,也不能實現
Externalizable
。 - record可用作代理對象來序列化其他對象。
readResolve()
方法可以返回record。也可以在record中添加writeReplace()。
- 反序列化record始終調用標準構造函數。因此,在此構造函數中添加的所有驗證規則都將在反序列化record時強制執行。
這使得record在應用程式中作為數據傳輸對象非常合適。
在實際場景中使用record
record是一個多功能的概念,您可以在許多上下文中使用。
第一種方法是在應用程式的對象模型中攜帶數據。用record充當不可變的數據載體,也是它們的設計目的。
由於可以聲明本地record,因此還可以使用它們來提高代碼的可讀性。
讓我們考慮以下場景。您有兩個建模為record的實體:City
,State
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,可用於創建任何類型的記憶體中結構來存儲流處理的數據。可以在Collection
或Map
的任何實例中進行收集,它可用來創建字元串,並且您可以創建自己的Collector
實例以將自己的結構添加到列表中。
將使用的大多數collector都可以使用 Collectors
工廠類的工廠方法之一創建。這是您在編寫 Collectors.toList() 或 Collectors.toSet()
時所做的。使用這些方法創建的一些collector可以組合使用,從而產生更多的collector。本教程涵蓋了所有這些要點。
如果在此工廠類中找不到所需的內容,則可以決定通過實現 Collector
介面來創建自己的collector。本教程還介紹瞭如何實現此介面。
Collector API 在 Stream 介面和專用數字流IntStream
、LongStream
和 DoubleStream
中的處理方式不同:。Stream
介面有兩個 collect()
方法重載,而數字流只有一個。缺少的正是將collector對象作為參數的那個。因此,不能將collector對象與專用的數字流一起使用。
在集合中收集
Collectors
工廠類提供了三種方法,用於在Collection
介面的實例中收集流的元素。
toList()
將它們收集在List
對象中。toSet()
將它們收集在Set
對象中。- 如果需要任何其他
Collection
實現,可以使用toCollection(supplier),
其中supplier
參數將用於創建所需的Collection
對象。如果您需要在LinkedList
實例中收集您的數據,您應該使用此方法。
代碼不應依賴於這些方法當前返回的 List
或 Set
的確切實現,因為它不是標準的一部分。
您還可以使用 unmodifiableList()
和 toUnmodifiableSet()
兩種方法獲取 List
和 Set
的不可變實現。
以下示例顯示了此模式的實際應用。首先,讓我們在一個普通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()
工廠方法創建的。
流的所有元素都將綁定到布爾值true
或false
。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()
模式。此模式適用於兩個函數,這兩個函數都應用於流的元素。
- 第一個稱為密鑰mapper,用於創建密鑰。
- 第二個稱為值mapper,用於創建值。
此collector的使用場景與 Collectors.groupingBy()
不同。特別是,它不處理流的多個元素生成相同密鑰的情況。這種情況下,預設情況下會引發IllegalStateException
。
這個collector能非常方便的創建緩存。假設User
類有一個類型為 Long
的primaryKey
屬性。您可以使用以下代碼創建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()
方法的三個參數如下:
element -> element.length()
是鍵mapper。element -> element
是值mapper。(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獲取的集合之一。
entrySet()
方法的映射集。keySet()
方法的鍵集。- 或者使用
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的鍵是每個長度在原始流中存在的次數。它是NumberOfLength
record的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::length
對NumberOfLength
做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為int
,long
,double
,然後對它們求和。averageagingInt()
、averageagingLong()
和averageagingDouble()
.這三種方法也將mapping函數作為參數,在計算平均值之前分別將流的元素map為int
,long
,double
。這些collector的工作方式與