Stream API
Stream API 是按照map/filter/reduce方法處理內(nèi)存中數(shù)據(jù)的最佳工具。
本系列教程由Record講起,然后結(jié)合Optional,討論collector的設(shè)計(jì)。
使用Record對(duì)不可變數(shù)據(jù)進(jìn)行建模
Java 語言為您提供了幾種創(chuàng)建不可變類的方法??赡茏钪苯拥氖莿?chuàng)建一個(gè)包含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;
}
}
編寫這些元素后,需要為字段添加訪問器。您還將添加一個(gè) toString
() 方法,可能還有一個(gè) equals
() 以及一個(gè) hashCode()
方法。手寫所有這些非常乏味且容易出錯(cuò),幸運(yùn)的是,您的 IDE 可以為您生成這些方法。
如果需要通過網(wǎng)絡(luò)或文件系統(tǒng)將此類的實(shí)例從一個(gè)應(yīng)用程序傳送到另一個(gè)應(yīng)用程序,則還可以考慮使此類可序列化。如果這樣做,還要添加一些有關(guān)如何序列化的信息。JDK 為您提供了幾種控制序列化的方法。
最后,您的Point
類可能有一百多行,主要是IDE 生成的代碼,只是為了對(duì)需要寫入文件的兩個(gè)整數(shù)不可變集進(jìn)行建模。
Record已經(jīng)添加到 JDK 以改變這一切。只需一行代碼即可為您提供所有這些。您需要做的就是聲明record的狀態(tài);其余部分由編譯器為您生成。
呼叫Record支援
Record可幫助您使此代碼更簡(jiǎn)單。從 Java SE 14 開始,您可以編寫以下代碼。
public record Point(int x, int y) {}
這一行代碼為您創(chuàng)建以下元素。
- 它是一個(gè)不可變的類,有兩個(gè)字段:
x
和y
- 它有一個(gè)標(biāo)準(zhǔn)的構(gòu)造函數(shù),用于初始化這兩個(gè)字段。
-
toString
()、equals
() 和hashCode()
方法是由編譯器為您創(chuàng)建的,其默認(rèn)行為與 IDE 將生成的內(nèi)容相對(duì)應(yīng)。如果需要,可以通過添加自己的實(shí)現(xiàn)來修改此行為。 - 它可以實(shí)現(xiàn)
Serializable
接口,以便您可以通過網(wǎng)絡(luò)或通過文件系統(tǒng)發(fā)送到其他應(yīng)用程序。序列化和反序列化record的方式遵循本教程末尾介紹的一些特殊規(guī)則。
record使創(chuàng)建不可變的數(shù)據(jù)集變得更加簡(jiǎn)單,無需任何 IDE 的幫助。降低了錯(cuò)誤的風(fēng)險(xiǎn),因?yàn)槊看涡薷膔ecord的組件時(shí),編譯器都會(huì)自動(dòng)更新 equals(
) 和 hashCode()
方法。
record的類
record也是類,是用關(guān)鍵字record
而不是class
聲明的類。讓我們聲明以下record。
public record Point(int x, int y) {}
編譯器在創(chuàng)建record時(shí)為您創(chuàng)建的類是final的。
此類繼承了 java.lang.Record
類。因此,您的record不能繼承其他任何類。
一條record可以實(shí)現(xiàn)任意數(shù)量的接口。
聲明record的組成部分
緊跟record名稱的塊是(int x, int y)
。它聲明了record組件。對(duì)于record的每個(gè)組件,編譯器都會(huì)創(chuàng)建一個(gè)同名的私有final字段。您可以在record中聲明任意數(shù)量的組件。
除了字段,編譯器還為每個(gè)組件生成一個(gè)訪問器。此訪問器跟組件的名稱相同,并返回其值。對(duì)于此record,生成的兩個(gè)方法如下。
public int x() {
return this.x;
}
public int y() {
return this.y;
}
如果此實(shí)現(xiàn)適用于您的應(yīng)用程序,則無需添加任何內(nèi)容。不過,也可以定義自己的訪問器。
編譯器為您生成的最后一個(gè)元素是 Object
類中 toString()、
equals
() 和 hashCode()
方法的重寫。如果需要,您可以定義自己對(duì)這些方法的覆蓋。
無法添加到record的內(nèi)容
有三件事不能添加到record中:
- 額外聲明的實(shí)例字段。不能添加任何與組件不對(duì)應(yīng)的實(shí)例字段。
- 實(shí)例字段的初始化。
- 實(shí)例的初始化塊。
您可以使用靜態(tài)字段,靜態(tài)初始化塊。
使用標(biāo)準(zhǔn)構(gòu)造函數(shù)構(gòu)造record
編譯器還會(huì)為您創(chuàng)建一個(gè)構(gòu)造函數(shù),稱為標(biāo)準(zhǔn)構(gòu)造函數(shù) canonical constructor。此構(gòu)造函數(shù)以record的組件作為參數(shù),并將其值復(fù)制到字段中。
在某些情況下,您需要覆蓋此默認(rèn)行為。讓我們研究?jī)煞N情況:
- 您需要驗(yàn)證組件的狀態(tài)
- 您需要制作可變組件的副本。
使用緊湊構(gòu)造函數(shù)
可以使用兩種不同的語法來重新定義record的標(biāo)準(zhǔn)構(gòu)造函數(shù)??梢允褂镁o湊構(gòu)造函數(shù)或標(biāo)準(zhǔn)構(gòu)造函數(shù)本身。
假設(shè)您有以下record。
public record Range(int start, int end) {}
對(duì)于該名稱的record,應(yīng)該預(yù)期 end
大于start
.您可以通過在record中編寫緊湊構(gòu)造函數(shù)來添加驗(yàn)證規(guī)則。
public record Range(int start, int end) {
public Range {//不需要參數(shù)塊
if (end <= start) {
throw new IllegalArgumentException("End cannot be lesser than start");
}
}
}
緊湊構(gòu)造函數(shù)不需要聲明其參數(shù)塊。
請(qǐng)注意,如果選擇此語法,則無法直接分配record的字段,例如this.start = start
- 這是通過編譯器添加代碼為您完成的。 但是,您可以為參數(shù)分配新值,這會(huì)導(dǎo)致相同的結(jié)果,因?yàn)榫幾g器生成的代碼隨后會(huì)將這些新值分配給字段。
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;
}
使用標(biāo)準(zhǔn)構(gòu)造函數(shù)
如果您更喜歡非緊湊形式(例如,因?yàn)槟幌胫匦路峙鋮?shù)),則可以自己定義標(biāo)準(zhǔn)構(gòu)造函數(shù),如以下示例所示。
public record Range(int start, int end) {//跟緊湊構(gòu)造不能共存
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;
}
}
}
這種情況下,您編寫的構(gòu)造函數(shù)需要為record的字段手動(dòng)賦值。
如果record的組件是可變的,則應(yīng)考慮在標(biāo)準(zhǔn)構(gòu)造函數(shù)和訪問器中制作它們的副本。
自定義構(gòu)造函數(shù)
還可以向record添加自定義構(gòu)造函數(shù),只要此構(gòu)造函數(shù)內(nèi)調(diào)用record的標(biāo)準(zhǔn)構(gòu)造函數(shù)即可。語法與經(jīng)典語法相同。對(duì)于任何類,調(diào)用this()
必須是構(gòu)造函數(shù)的第一個(gè)語句。
讓我們檢查以下State
record。它由三個(gè)組件定義:
- 此州的名稱
- 該州首府的名稱
- 城市名稱列表,可能為空。
我們需要存儲(chǔ)城市列表的副本,確保它不會(huì)從此record的外部修改。 這可以通過使用緊湊形式,將參數(shù)重新分配給副本。
擁有一個(gè)不用城市作參數(shù)的構(gòu)造函數(shù)在您的應(yīng)用程序中很有用。這可以是另一個(gè)構(gòu)造函數(shù),它只接收州名和首都名。第二個(gè)構(gòu)造函數(shù)必須調(diào)用標(biāo)準(zhǔn)構(gòu)造函數(shù)。
然后,您可以將城市作為 vararg 傳遞。為此,您可以創(chuàng)建第三個(gè)構(gòu)造函數(shù)。
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));//也是不可變的
}
}
請(qǐng)注意,List.copyOf()
方法的參數(shù)不接受空值。
獲取record的狀態(tài)
您不需要向record添加任何訪問器,因?yàn)榫幾g器會(huì)為您執(zhí)行此操作。一條record的每個(gè)組件都有一個(gè)訪問器方法,該方法具有此組件的名稱。
但是,某些情況下,您需要定義自己的訪問器。 例如,假設(shè)上一節(jié)中的State
record在構(gòu)造期間沒有創(chuàng)建列表的不可修改的副本 - 那么它應(yīng)該在訪問器中執(zhí)行此操作,以確保調(diào)用方無法改變其內(nèi)部狀態(tài)。 您可以在record中添加以下代碼以返回此副本。
public List<String> cities() {
return List.copyOf(cities);
}
序列化record
如果您的record類實(shí)現(xiàn)了可序列化,則可以序列化和反序列化
record。不過也有限制。
- 可用于替換默認(rèn)序列化過程的任何系統(tǒng)都不適用于record。創(chuàng)建 writeObject() 和 readObject() 方法不起作用,也不能實(shí)現(xiàn)
Externalizable
。 - record可用作代理對(duì)象來序列化其他對(duì)象。
readResolve()
方法可以返回record。也可以在record中添加writeReplace()。
- 反序列化record始終調(diào)用標(biāo)準(zhǔn)構(gòu)造函數(shù)。因此,在此構(gòu)造函數(shù)中添加的所有驗(yàn)證規(guī)則都將在反序列化record時(shí)強(qiáng)制執(zhí)行。
這使得record在應(yīng)用程序中作為數(shù)據(jù)傳輸對(duì)象非常合適。
在實(shí)際場(chǎng)景中使用record
record是一個(gè)多功能的概念,您可以在許多上下文中使用。
第一種方法是在應(yīng)用程序的對(duì)象模型中攜帶數(shù)據(jù)。用record充當(dāng)不可變的數(shù)據(jù)載體,也是它們的設(shè)計(jì)目的。
由于可以聲明本地record,因此還可以使用它們來提高代碼的可讀性。
讓我們考慮以下場(chǎng)景。您有兩個(gè)建模為record的實(shí)體:City
,State
public record City(String name, State state) {}
public record State(String name) {}
假設(shè)您有一個(gè)城市列表,您需要計(jì)算擁有最多城市數(shù)量的州??梢允褂?Stream API 首先使用每個(gè)州擁有的城市數(shù)構(gòu)建各州的柱狀圖。此柱狀圖由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();
最后一段代碼是技術(shù)性的;它不具有任何業(yè)務(wù)意義;因?yàn)槭褂?code>Map.Entry實(shí)例對(duì)柱狀圖的每個(gè)元素進(jìn)行建模。
使用本地record可以大大改善這種情況。下面的代碼創(chuàng)建一個(gè)新的record類,該類包含一個(gè)州和該州的城市數(shù)。它有一個(gè)構(gòu)造函數(shù),該構(gòu)造函數(shù)將 Map.Entry
的實(shí)例作為參數(shù),將鍵值對(duì)流映射到record流。
由于需要按城市數(shù)比較這些集,因此可以添加工廠方法來提供此比較器。代碼將變?yōu)橐韵聝?nèi)容。
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();
您的代碼現(xiàn)在以有意義的方式提取最大值。您的代碼更具可讀性,更易于理解,不易出錯(cuò),從長(zhǎng)遠(yuǎn)來看更易于維護(hù)。
使用collector作為末端操作
讓我們回到Stream API。
使用collector收集流元素
您已經(jīng)使用了一個(gè)非常有用的模式collect(Collectors.toList())
來收集由 List
中的流處理的元素。此 collect()
方法是在 Stream
接口中定義的末端方法,它將 Collector
類型的對(duì)象作為參數(shù)。此Collector
接口定義了自己的 API,可用于創(chuàng)建任何類型的內(nèi)存中結(jié)構(gòu)來存儲(chǔ)流處理的數(shù)據(jù)??梢栽?code>Collection或Map
的任何實(shí)例中進(jìn)行收集,它可用來創(chuàng)建字符串,并且您可以創(chuàng)建自己的Collector
實(shí)例以將自己的結(jié)構(gòu)添加到列表中。
將使用的大多數(shù)collector都可以使用 Collectors
工廠類的工廠方法之一創(chuàng)建。這是您在編寫 Collectors.toList() 或 Collectors.toSet()
時(shí)所做的。使用這些方法創(chuàng)建的一些collector可以組合使用,從而產(chǎn)生更多的collector。本教程涵蓋了所有這些要點(diǎn)。
如果在此工廠類中找不到所需的內(nèi)容,則可以決定通過實(shí)現(xiàn) Collector
接口來創(chuàng)建自己的collector。本教程還介紹了如何實(shí)現(xiàn)此接口。
Collector API 在 Stream 接口和專用數(shù)字流IntStream
、LongStream
和 DoubleStream
中的處理方式不同:。Stream
接口有兩個(gè) collect()
方法重載,而數(shù)字流只有一個(gè)。缺少的正是將collector對(duì)象作為參數(shù)的那個(gè)。因此,不能將collector對(duì)象與專用的數(shù)字流一起使用。
在集合中收集
Collectors
工廠類提供了三種方法,用于在Collection
接口的實(shí)例中收集流的元素。
-
toList()
將它們收集在List
對(duì)象中。 -
toSet()
將它們收集在Set
對(duì)象中。 - 如果需要任何其他
Collection
實(shí)現(xiàn),可以使用toCollection(supplier),
其中supplier
參數(shù)將用于創(chuàng)建所需的Collection
對(duì)象。如果您需要在LinkedList
實(shí)例中收集您的數(shù)據(jù),您應(yīng)該使用此方法。
代碼不應(yīng)依賴于這些方法當(dāng)前返回的 List
或 Set
的確切實(shí)現(xiàn),因?yàn)樗皇菢?biāo)準(zhǔn)的一部分。
您還可以使用 unmodifiableList()
和 toUnmodifiableSet()
兩種方法獲取 List
和 Set
的不可變實(shí)現(xiàn)。
以下示例顯示了此模式的實(shí)際應(yīng)用。首先,讓我們?cè)谝粋€(gè)普通List
實(shí)例中收集。
List<Integer> numbers =
IntStream.range(0, 10)
.boxed()//需要裝箱
.collect(Collectors.toList());
System.out.println("numbers = " + numbers);
此代碼使用 boxed()
中繼方法從 IntStream.range() 創(chuàng)建的 IntStream
創(chuàng)建一個(gè) Stream
,方法是對(duì)該流的所有元素進(jìn)行裝箱。運(yùn)行此代碼將打印以下內(nèi)容。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
第二個(gè)示例創(chuàng)建一個(gè)只有偶數(shù)且沒有重復(fù)項(xiàng)的 HashSet
。
Set<Integer> evenNumbers =
IntStream.range(0, 10)
.map(number -> number / 2)
.boxed()
.collect(Collectors.toSet());
System.out.println("evenNumbers = " + evenNumbers);
運(yùn)行此代碼將產(chǎn)生以下結(jié)果。
evenNumbers = [0, 1, 2, 3, 4]
最后一個(gè)示例使用 Supplier
對(duì)象來創(chuàng)建用于收集流元素的 LinkedList
實(shí)例。
LinkedList<Integer> linkedList =
IntStream.range(0, 10)
.boxed()
.collect(Collectors.toCollection(LinkedList::new));
System.out.println("linked listS = " + linkedList);
運(yùn)行此代碼將產(chǎn)生以下結(jié)果。
linked list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
使用collector計(jì)數(shù)
Collectors
工廠類為您提供了幾種方法來創(chuàng)建collector,這些collector執(zhí)行的操作與普通末端方法為您提供的操作相同。Collectors.counting
() 工廠方法就是這種情況,它與在流上調(diào)用 count()
相同。
這是值得注意的,您可能想知道為什么使用兩種不同的模式實(shí)現(xiàn)了兩次這樣的功能。將在下一節(jié)有關(guān)在map中收集時(shí)回答此問題,您將在其中組合collector以創(chuàng)建更多collector。
目前,編寫以下兩行代碼會(huì)導(dǎo)致相同的結(jié)果。
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);
運(yùn)行此代碼將產(chǎn)生以下結(jié)果。
count = 3
countWithACollector = 3
收集在字符串中
Collectors
工廠類提供的另一個(gè)非常有用的collector是 joining()
。此collector僅適用于字符串流,并將該流的元素連接為單個(gè)字符串。它有幾個(gè)重載。
- 第一個(gè)將分隔符作為參數(shù)。
- 第二個(gè)將分隔符、前綴和后綴作為參數(shù)。
讓我們看看這個(gè)collector的實(shí)際效果。
String joined =
IntStream.range(0, 10)
.boxed()
.map(Object::toString)
.collect(Collectors.joining());
System.out.println("joined = " + joined);
運(yùn)行此代碼將生成以下結(jié)果。
joined = 0123456789
可以使用以下代碼向此字符串添加分隔符。
String joined =
IntStream.range(0, 10)
.boxed()
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println("joined = " + joined);
結(jié)果如下。
joined = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
讓我們看看最后一個(gè)重載,它接收分隔符、前綴和后綴。
String joined =
IntStream.range(0, 10)
.boxed()
.map(Object::toString)
.collect(Collectors.joining(", ", "{"), "}");
System.out.println("joined = " + joined);
結(jié)果如下。
joined = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
請(qǐng)注意,此collector可以正確處理流為空或僅處理單個(gè)元素的極端情況。
當(dāng)您需要生成此類字符串時(shí),此collector非常方便。即使您前面的數(shù)據(jù)不在集合中或只有幾個(gè)元素,您也可能想使用它。如果是這種情況,使用 String.join()
工廠類或 StringJoiner
對(duì)象都將正常工作,無需支付創(chuàng)建流的開銷。
使用Predicate對(duì)元素進(jìn)行分區(qū)
Collector API 提供了三種模式,用于從流的元素創(chuàng)建map。我們介紹的第一個(gè)使用布爾鍵創(chuàng)建map。它是使用 partitionningBy()
工廠方法創(chuàng)建的。
流的所有元素都將綁定到布爾值true
或false
。map將綁定到每個(gè)值的所有元素存儲(chǔ)在列表中。因此,如果將此collector應(yīng)用于Stream
,它將生成具有以下類型的map:Map<Boolean,List<T>>
。
測(cè)試的Predicate應(yīng)作為參數(shù)提供給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));
運(yùn)行此代碼將生成以下結(jié)果。
false :: [one, two, four, five, six, nine, ten]
true :: [three, seven, eight, eleven, twelve]
此工廠方法具有重載,它將另一個(gè)collector作為參數(shù)。此collector稱為下游collector。我們將在本教程的下一段中介紹,屆時(shí)我們將介紹 groupingBy()
。
在map中收集并進(jìn)行分組
我們提供的第二個(gè)collector非常重要,因?yàn)樗试S您創(chuàng)建柱狀圖。
對(duì)map中的流元素進(jìn)行分組
可用于創(chuàng)建柱狀圖的collector是使用 Collectors.groupingBy()
方法創(chuàng)建的。此方法具有多個(gè)重載。
collector將創(chuàng)建map。通過對(duì)其應(yīng)用 Function
實(shí)例,為流的每個(gè)元素計(jì)算一個(gè)鍵。此函數(shù)作為 groupingBy()
方法的參數(shù)提供。它在Collector API 中稱為分類器 classifier。
除了不應(yīng)該返回 null 之外,此函數(shù)沒有任何限制。
此函數(shù)可能會(huì)為流的多個(gè)元素返回相同的鍵。groupingBy()
支持這一點(diǎn),并將所有這些元素收集在一個(gè)列表中。
因此,如果您正在處理 Stream
并使用 Function<
T, K> 作為分類器,則 groupingBy()
會(huì)創(chuàng)建一個(gè) 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));
此示例中使用的分類器是一個(gè)函數(shù),用于從該流返回每個(gè)字符串的長(zhǎng)度。因此,map按字符串長(zhǎng)度將字符串分組到列表中。它具有Map<Interger,List<String>>
的類型。
運(yùn)行此代碼將打印以下內(nèi)容。
3 :: [one, two, six, ten]
4 :: [four, five, nine]
5 :: [three, seven, eight]
6 :: [eleven, twelve]
對(duì)分組后的值進(jìn)行處理
計(jì)算數(shù)量
groupingBy()
方法還接受另一個(gè)參數(shù),即另一個(gè)collector。此collector在Collector API 中稱為下游collector,但它沒有什么特別的。使它成為下游collector的原因只是,它作為參數(shù)傳遞給前一個(gè)collector的創(chuàng)建。
此下游collector用于收集由 groupingBy()
創(chuàng)建的map的值。
在前面的示例中,groupingBy()
創(chuàng)建了一個(gè)map,其值是字符串列表。如果為 groupingBy()
方法提供下游collector,API 將逐個(gè)流式傳輸這些列表,并使用下游collector收集這些流。
假設(shè)您將 Collectors.counting()
作為下游collector傳遞。將計(jì)算的內(nèi)容如下。
[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 代碼,因此您無法執(zhí)行它。它只是在那里解釋如何使用這個(gè)下游collector。
下面將創(chuàng)建的map取決于您提供的下游collector。鍵不會(huì)修改,但值可能會(huì)。在 Collectors.counting()
的情況下,值將轉(zhuǎn)換為 Long
。然后,map的類型將變?yōu)?Map<Integer,Long>
。
前面的示例變?yōu)橐韵聝?nèi)容。
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>轉(zhuǎn)為Stream向下傳遞,變成Long
map.forEach((key, value) -> System.out.println(key + " :: " + value));
運(yùn)行此代碼將打印以下結(jié)果。它給出了每個(gè)長(zhǎng)度的字符串?dāng)?shù),這是字符串長(zhǎng)度的柱狀圖。
3 :: 4
4 :: 3
5 :: 3
6 :: 2
連接列表的值
您還可以將 Collectors.joining()
collector作為下游collector傳遞,因?yàn)榇薽ap的值是字符串列表。請(qǐng)記住,此collector只能用于字符串流。這將創(chuàng)建 Map<Integer,String>
的實(shí)例。您可以將上一個(gè)示例更改為以下內(nèi)容。
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));
運(yùn)行此代碼將生成以下結(jié)果。
3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve
控制map的實(shí)例
此 groupingBy()
方法的最后一個(gè)重載將supplier
的實(shí)例作為參數(shù),以便您控制需要此collector創(chuàng)建的 Map
實(shí)例。
您的代碼不應(yīng)依賴于 groupingBy()
返回的確切map類型,因?yàn)樗皇菢?biāo)準(zhǔn)的一部分。
使用ToMap在map中收集
Collector API 為您提供了創(chuàng)建map的第二種模式:Collectors.toMap()
模式。此模式適用于兩個(gè)函數(shù),這兩個(gè)函數(shù)都應(yīng)用于流的元素。
- 第一個(gè)稱為密鑰mapper,用于創(chuàng)建密鑰。
- 第二個(gè)稱為值mapper,用于創(chuàng)建值。
此collector的使用場(chǎng)景與 Collectors.groupingBy()
不同。特別是,它不處理流的多個(gè)元素生成相同密鑰的情況。這種情況下,默認(rèn)情況下會(huì)引發(fā)IllegalStateException
。
這個(gè)collector能非常方便的創(chuàng)建緩存。假設(shè)User
類有一個(gè)類型為 Long
的primaryKey
屬性。您可以使用以下代碼創(chuàng)建User
對(duì)象的緩存。
List<User> users = ...;
Map<Long, User> userCache =
users.stream()
.collect(User::getPrimaryKey,
Function.idendity());//key必須不同
使用 Function.identity()
工廠方法只是告訴collector不要轉(zhuǎn)換流的元素。
如果您希望流的多個(gè)元素生成相同的鍵,則可以將進(jìn)一步的參數(shù)傳遞給 toMap()
方法。此參數(shù)的類型為 BinaryOperator
。當(dāng)檢測(cè)到?jīng)_突元素時(shí),實(shí)現(xiàn)將它應(yīng)用于沖突元素。然后,您的binary operator將生成一個(gè)結(jié)果,該結(jié)果將代替先前的值放入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()
方法的三個(gè)參數(shù)如下:
-
element -> element.length()
是鍵mapper。 -
element -> element
是值mapper。 -
(element1, element2) -> element1 + ", " + element2)
是合并函數(shù),相同鍵的兩個(gè)元素會(huì)調(diào)用。
運(yùn)行此代碼將生成以下結(jié)果。
3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve
另外也可以將supplier作為參數(shù)傳遞給 toMap()
方法,以控制此collector將使用的 Map
接口實(shí)例。
toMap()
collector有一個(gè)孿生方法 toConcurrentMap(),
它將在并發(fā)map中收集數(shù)據(jù)。實(shí)現(xiàn)不保證map的確切類型。
從柱狀圖中提取最大值
groupingBy()
是分析計(jì)算柱狀圖的最佳模式。讓我們研究一個(gè)完整的示例,其中您構(gòu)建柱狀圖,然后嘗試根據(jù)要求找到其中的最大值。
提取唯一的最大值
您要分析的柱狀圖如下。它看起來像我們?cè)谇懊娴氖纠惺褂玫哪莻€(gè)。
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));
打印此柱狀圖將得到以下結(jié)果。
3 :: 4 //期望是4 =>3
4 :: 3
5 :: 3
6 :: 2
從此柱狀圖中提取最大值應(yīng)得到結(jié)果:3 :: 4
。Stream API 具有提取最大值所需的所有工具。不幸的是,Map
接口上沒有stream()
方法。要在map上創(chuàng)建流,您首先需要獲取可以從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作為參數(shù)。實(shí)際上,Map.Entry
接口的確有幾個(gè)工廠方法來創(chuàng)建這樣的comparator。我們?cè)诖耸纠惺褂玫倪@個(gè),創(chuàng)建了一個(gè)可以比較 Map.Entry
實(shí)例的comparator,使用這些鍵值對(duì)的值。僅當(dāng)值實(shí)現(xiàn)Comparable
接口時(shí),此比較才有效。
這種代碼模式非常普通,只要具有可比較的值,就可以在任何map上使用。我們可以使其特別一點(diǎn),更具可讀性,這要?dú)w功于Java SE 16中記錄Record的引入。
讓我們創(chuàng)建一個(gè)record來模擬此map的鍵值對(duì)。創(chuàng)建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,以前的模式將變?yōu)橐韵聝?nèi)容。
NumberOfLength maxNumberOfLength =
histogram.entrySet().stream()
.map(NumberOfLength::fromEntry)
.max(NumberOfLength.comparingByLength())//Record替換Entry,后面要引用字段
.orElseThrow();
System.out.println("maxNumberOfLength = " + maxNumberOfLength);
運(yùn)行此示例將打印出以下內(nèi)容。
maxNumberOfLength = NumberOfLength[length=3, number=4]
您可以看到此record看起來像 Map.Entry
接口。它有一個(gè)mapping鍵值對(duì)的工廠方法和一個(gè)用于創(chuàng)建comparator的工廠方法。柱狀圖的分析變得更加可讀和易于理解。
提取多個(gè)最大值
前面的示例是一個(gè)很好的示例,因?yàn)榱斜碇兄挥幸粋€(gè)最大值。不幸的是,現(xiàn)實(shí)生活中的情況通常不是那么好,您可能有幾個(gè)與最大值匹配的鍵值對(duì)。
讓我們從上一個(gè)示例的集合中刪除一個(gè)元素。
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));
打印此柱狀圖將得到以下結(jié)果。
3 :: 3
4 :: 3
5 :: 3//期望是3 =>[3,4,5]
6 :: 2
現(xiàn)在我們有三個(gè)鍵值對(duì)的最大值。如果使用前面的代碼模式提取它,則將選擇并返回這三個(gè)中的一個(gè),隱藏其他兩個(gè)。
解決此問題的解決方案是創(chuàng)建另一個(gè)map,其中鍵是字符串?dāng)?shù)量,值是與之匹配的長(zhǎng)度。換句話說:您需要反轉(zhuǎn)此map。對(duì)于 groupingBy()
來說,這是一個(gè)很好的場(chǎng)景。此示例將在本部分的后面介紹,因?yàn)槲覀冞€需要一個(gè)元素來編寫此代碼。
使用中繼collector
到目前為止,我們介紹的collector只是計(jì)數(shù)、連接和收集到列表或map中。它們都屬于末端操作。Collector API 也提供了執(zhí)行中繼操作的其他collector:mapping、filtering和flatmapping。您可能想知道這樣的意義是什么。事實(shí)上,這些特殊的collector并不能單獨(dú)創(chuàng)建。它們的工廠方法都需要下游collector作為第二個(gè)參數(shù)。
也就是說,您這樣創(chuàng)建的整體collector是中繼操作和末端操作的組合。
使用collector來mapping
我們可以檢查的第一個(gè)中繼操作是mapping操作。mapping collector是使用 Collectors.mapping()
工廠方法創(chuàng)建的。它將常規(guī)mapping函數(shù)作為第一個(gè)參數(shù),將必需的下游collector作為第二個(gè)參數(shù)。
在下面的示例中,我們將mapping與列表中mapping后的元素的集合相結(jié)合。
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()
工廠方法創(chuàng)建一個(gè)常規(guī)collector。您可以將此collector作為下游collector傳遞給任何接受collector的方法,例如,包括 groupingBy()
或 toMap()。
您可能還記得在“提取多個(gè)最大值”一節(jié)中,我們留下了一個(gè)關(guān)于反轉(zhuǎn)map的懸而未決的問題。讓我們使用這個(gè)mapping collector來解決問題。
在此示例中,您創(chuàng)建了一個(gè)柱狀圖?,F(xiàn)在,您需要使用 groupingBy()
反轉(zhuǎn)此柱狀圖以查找所有最大值。
以下代碼創(chuàng)建此類map。
Map<Integer, Long> histogram = ...;
var map =
histogram.entrySet().stream()
.map(NumberOfLength::fromEntry)
.collect(
Collectors.groupingBy(NumberOfLength::number));//<Long, List<NumberOfLength>>
讓我們檢查此代碼并確定所構(gòu)建map的確切類型。
此map的鍵是每個(gè)長(zhǎng)度在原始流中存在的次數(shù)。它是NumberOfLength
record的number
部分,Long。
類型。
這些值是此流的元素,收集到列表中。因此,是NumberOfLength
的對(duì)象列表。這張map的確切類型是Map<Long,NumberOfLength>
。
當(dāng)然,這不是您所要的。您需要的只是字符串的長(zhǎng)度,而不是record。從record中提取組件是一個(gè)mapping過程。您需要將這些NumberOfLength
實(shí)例mapping為其length
組件?,F(xiàn)在我們介紹了mapping collector,可以解決這一點(diǎn)。您需要做的就是將正確的下游collector添加到 groupingBy()
調(diào)用中。
代碼將變?yōu)橐韵聝?nèi)容。
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>>
構(gòu)建的map的值現(xiàn)在是使用NumberOfLength::length
對(duì)NumberOfLength
做mapping后生成的對(duì)象列表。此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);
運(yùn)行此代碼將生成以下內(nèi)容。
result = 3=[3, 4, 5]//最多的length列表
這意味著有三種長(zhǎng)度的字符串在此流中出現(xiàn)三次:3、4 和 5。
此示例顯示嵌套在另外兩個(gè)collector中的collector,在使用此 API 時(shí),這種情況經(jīng)常發(fā)生。乍一看可能看起來很嚇人,但它只是使用下游collector組合成了collector。
您可以看到為什么擁有這些中繼collector很有趣。通過使用collector提供的中繼操作,您可以為幾乎任何類型的處理創(chuàng)建下游collector,從而對(duì)map的值進(jìn)行后續(xù)處理。
使用collector進(jìn)行filtering和flatmapping
filtering collector遵循與mapping collector相同的模式。它是使用 Collectors.filtering()
工廠方法創(chuàng)建的,該方法接收常規(guī)Predicate來filter數(shù)據(jù),同時(shí)要有必需的下游collector。
由 Collectors.flatMapping()
工廠方法創(chuàng)建的flatmapping collector也是如此,它接收flatmapping函數(shù)(返回流的函數(shù))和必需的下游collector。
使用末端collector
Collector API 還提供了幾個(gè)末端操作,對(duì)應(yīng)于Stream API 上可用的末端操作。
-
maxBy()
和minBy()。
這兩個(gè)方法都將comparator作為參數(shù),如果處理的流本身為空,則返回一個(gè)Optional對(duì)象。 -
summingInt
()、summingLong()
和summingDouble()。
這三種方法將mapping函數(shù)作為參數(shù),分別將流的元素mapping為int
,long
,double
,然后對(duì)它們求和。 -
averageagingInt()
、averageagingLong()
和averageagingDouble()
.這三種方法也將mapping函數(shù)作為參數(shù),在計(jì)算平均值之前分別將流的元素map為int
,long
,double
。這些collector的工作方式與IntStream
、LongStream
和DoubleStream
中定義的相應(yīng)average()
方法不同。它們都返回一個(gè)Double
實(shí)例,對(duì)于空流返回 0。而數(shù)字流的average()
方法返回一個(gè)Optional對(duì)象,該對(duì)象對(duì)于空流為空。
創(chuàng)建自己的collector
了解collector的工作原理
如前所述,Collectors
工廠類僅處理對(duì)象流,因?yàn)閷ollector對(duì)象作為參數(shù)的 collect()
方法僅存在于 Stream
中。如果您需要收集數(shù)字流,那么您需要了解collector的組成元素是什么。
簡(jiǎn)單說,collector建立在四個(gè)基本組件之上。前兩個(gè)用于收集流的元素。第三個(gè)僅用于并行流。某些類型的collector需要第四個(gè),這些collector需要對(duì)構(gòu)建的容器作后續(xù)處理。
第一個(gè)組件用于創(chuàng)建收集流元素的容器。此容器易于識(shí)別。例如,在上一部分介紹的情況下,我們使用了 ArrayList
類、HashSet
類和 HashMap
類??梢允褂?code>supplier實(shí)例對(duì)創(chuàng)建此類容器進(jìn)行建模。第一個(gè)組件稱為supplier。
第二個(gè)組件旨在將流中的單個(gè)元素添加到容器。Stream API 的實(shí)現(xiàn)將重復(fù)調(diào)用此操作,將流的所有元素逐個(gè)添加到容器中。
在Collector API中,此組件由BiConsumer
的實(shí)例建模。這個(gè)biconsumer有兩個(gè)參數(shù)。
- 第一個(gè)是容器本身,流的先前元素填充了部分。
- 第二個(gè)是應(yīng)添加的流元素。
此biconsumer在Collector API 的上下文中稱為accumulator。
這兩個(gè)組件應(yīng)該足以讓collector工作,但 Stream API 帶來了一個(gè)約束,使collector正常工作需要另外兩個(gè)組件。
你可能還記得,Stream API 支持并行化。本教程稍后將更詳細(xì)地介紹這一點(diǎn)。您需要知道的是,并行化將流的元素拆分為子流,每個(gè)元素都由 CPU 的內(nèi)核處理。Collector API 可以在這樣的上下文中工作:每個(gè)子流將只收集在自己的容器實(shí)例中。
處理完這些子流后,您將擁有多個(gè)容器,每個(gè)容器都包含它所處理的子流中的元素。這些容器是相同的,因?yàn)樗鼈兪桥c同一supplier一起創(chuàng)建的。現(xiàn)在,您需要一種方法將它們合并為一個(gè)。為了能夠做到這一點(diǎn),Collector API 需要第三個(gè)組件,即combiner,它將這些容器合并在一起。combiner由 BinaryOperator
的實(shí)例建模,該實(shí)例接收兩個(gè)部分填充的容器并返回一個(gè)。
Stream API 的 collect()
也有個(gè)重載,這個(gè) BinaryOperator
變成了 BiConsumer
,我們主要使用這個(gè)。
第四個(gè)組件稱為finisher,本部分稍后將介紹。
在集合中收集原始類型
使用前三個(gè)組件,您可以嘗試專用數(shù)字流中的 collect()
方法。IntStream.collect()
方法有三個(gè)參數(shù):
-
Supplier
的實(shí)例,稱為supplier; -
ObjIntConsumer
的實(shí)例,稱為accumulator; -
BiConsumer
的實(shí)例,稱為combiner。
讓我們編寫代碼以在List<Integer>
中收集IntStream
。
Supplier<List<Integer>> supplier = ArrayList::new;//容器
ObjIntConsumer<List<Integer>> accumulator = Collection::add;//元素如何進(jìn)入容器
BiConsumer<List<Integer>, List<Integer>> combiner = Collection::addAll;//多個(gè)片段如何合并
List<Integer> collect =
IntStream.range(0, 10)
.collect(supplier, accumulator, combiner );
System.out.println("collect = " + collect);
運(yùn)行此代碼將生成以下結(jié)果。
collect = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
將這些數(shù)據(jù)收集為Set只需要更改supplier
的實(shí)現(xiàn)并相應(yīng)地調(diào)整類型。
在 StringBuffer 中收集原始類型
讓我們研究一下如何自己實(shí)現(xiàn) Collectors.joining()
,以將原始類型流的元素連接在單個(gè)字符串中。String
類是不可變的,因此無法在其中累積元素。您可以使用可變的 StringBuffer
類。
在 StringBuffer
中收集元素遵循與前一個(gè)相同的模式。
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);
運(yùn)行此代碼將生成以下結(jié)果。
collect = 0123456789
使用finisher對(duì)collector進(jìn)行后續(xù)處理
你在上一段中編寫的代碼幾乎完成了你需要的:它在 StringBuffer
實(shí)例中連接字符串,你可以通過調(diào)用它的 toString()
方法來創(chuàng)建一個(gè)常規(guī)的 String
對(duì)象。但是 Collectors.joining()
collector直接生成一個(gè)字符串
,而無需你調(diào)用 toString()。
那么它是怎么做到的呢?
Collector API 精確地定義了第四個(gè)組件來處理這種情況,稱為finisher。finisher是一個(gè)Function
,它獲取累積元素的容器并將其轉(zhuǎn)換為其他內(nèi)容。在 Collectors.joining()
的情況下,這個(gè)函數(shù)只是下面的。
Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();
對(duì)很多collector來說,finisher只是恒等函數(shù)。比如:toList()
、toSet
()、groupingBy()
和 toMap()。
其他情況下,collector內(nèi)部使用的可變?nèi)萜鞒蔀橹欣^容器,在返回到應(yīng)用程序之前,該容器將mapping為其他對(duì)象(可能是另一個(gè)容器)。這就是Collector API 處理不可變列表、set或map創(chuàng)建的方式。finisher用于將中繼容器密封到不可變?nèi)萜髦?,返回到?yīng)用程序。
finisher還有其他用途,可以提高代碼的可讀性。Collectors
工廠類有一個(gè)工廠方法,我們還沒有介紹:collectingAndThen()
方法。此方法將collector作為第一個(gè)參數(shù),將finisher作為第二個(gè)參數(shù)。它會(huì)將第一個(gè)collector收集的結(jié)果,使用您提供的finisher對(duì)其進(jìn)行mapping。
您可能還記得以下示例,我們已經(jīng)在前面的部分中多次檢查過該示例。它是關(guān)于提取柱狀圖的最大值。
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);
第一步,您構(gòu)建了 Map<Integer,Long>
類型的柱狀圖,在第二步中,您提取了此柱狀圖的最大值,按值比較鍵值對(duì)。
第二步實(shí)際上是將map轉(zhuǎn)換為特殊的鍵/值對(duì)。您可以使用以下函數(shù)對(duì)其進(jìn)行建模。
Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher =
map -> map.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow();
此函數(shù)的類型起初可能看起來很復(fù)雜。事實(shí)上,它只是從map中提取一個(gè)鍵值對(duì),類型為 Map.Entry
。
現(xiàn)在您已經(jīng)有了這個(gè)函數(shù),您可以使用 collectingAndThen()
將此最大值提取步驟集成到collector本身中。然后,模式將變?yōu)橐韵聝?nèi)容。
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需要特別注意類型,消費(fèi)Map,產(chǎn)出Entry
Map.Entry<Integer, Long> maxValue =
strings.stream()
.collect(
Collectors.collectingAndThen(
Collectors.groupingBy(
String::length,
Collectors.counting()),
finisher
));
System.out.println("maxValue = " + maxValue);
您可能想知道為什么需要編寫此看起來非常復(fù)雜的代碼?
現(xiàn)在,您已經(jīng)擁有了由單個(gè)collector建模的最大值提取器,您可以將其用作另一個(gè)collector的下游collector。做到這一點(diǎn),可以組合更多的collector對(duì)您的數(shù)據(jù)進(jìn)行更復(fù)雜的計(jì)算。
將兩個(gè)collector的結(jié)果與三通collector相結(jié)合
在 Java SE 12 的 Collectors
類中添加了一個(gè)名為 teeing()
的方法。此方法需要兩個(gè)下游collector和一個(gè)合并函數(shù)。
讓我們通過一個(gè)場(chǎng)景,看看您可以使用collector做什么。想象一下,您有以下Car
,Truck
兩種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
對(duì)象有幾個(gè)組成部分:顏色、引擎、驅(qū)動(dòng)器以及它可以運(yùn)輸?shù)囊欢〝?shù)量的乘客。Truck
有引擎,有驅(qū)動(dòng)器,可以運(yùn)輸一定量的貨物。兩者都實(shí)現(xiàn)相同的接口:Vehicle
假設(shè)您有一系列Vehicle
,您需要找到所有配備電動(dòng)引擎的Car
。根據(jù)您的應(yīng)用程序,您可能會(huì)使用流filter您的Car
集合。或者,如果您知道下一個(gè)需求,將是找到配備混合動(dòng)力引擎的Car
,您可能更愿意準(zhǔn)備一個(gè)map,以引擎為鍵,并以配備該引擎的Car
列表作為值。在這兩種情況API 都會(huì)為你提供正確的模式來獲取所需的內(nèi)容。
假設(shè)您需要將所有電動(dòng)Truck
添加到此集合中。也有可能想一次處理所有Vehicle
,但是用于filter數(shù)據(jù)的Predicate變得越來越復(fù)雜。它可能如下所示。
Predicate<Vehicle> predicate =
vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC ||
vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;
//這個(gè)是instanceof新用法,后面直接賦值變量,同時(shí)跟短路操作
您真正需要的是以下內(nèi)容:
- filter
Vehicle
以獲得所有電動(dòng)Car
- filter
Vehicle
以獲得所有電動(dòng)Truck
- 合并兩個(gè)結(jié)果。
這正是teeing collector可以為您做的事情。teeing collector由 Collectors.teeing()
工廠方法創(chuàng)建,該方法接收三個(gè)參數(shù)。
- 第一個(gè)下游collector,用于收集流的數(shù)據(jù)。
- 第二個(gè)下游collector,也用于收集數(shù)據(jù)。
- 一個(gè)bifunction,用于合并由兩個(gè)下游collector創(chuàng)建的兩個(gè)容器。
您的數(shù)據(jù)將一次性處理,以保證最佳性能。
我們已經(jīng)介紹了使用collector來filter流元素的模式。合并函數(shù)只是對(duì) Collection.addAll()
方法的調(diào)用。以下是代碼:
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;
}));
實(shí)現(xiàn)collector接口
為什么要實(shí)現(xiàn)collector接口?
有三種方法可以創(chuàng)建自己的collector。
包括將現(xiàn)有collector與Collectors
工廠類結(jié)合,將collector作為下游collector傳遞給另一個(gè)collector,或者作為finisher一起使用 collectingAndThen()
。我們上面教程中已經(jīng)介紹過。
您還可以調(diào)用 collect()
方法,該方法接收構(gòu)建collector的三個(gè)元素。這些方法在原始類型流和對(duì)象流上都可用。他們接收了我們?cè)谇懊娌糠种刑岢龅娜齻€(gè)參數(shù)。
- 用于創(chuàng)建可變?nèi)萜鞯?em>supplier,其中累積了流的元素。
- accumulator,由biconsumer建模。
- combiner也由biconsumer建模,用于組合兩個(gè)部分填充的容器,用于并行流的情況。
第三種方法是自己實(shí)現(xiàn) Collector
接口,并將您的實(shí)現(xiàn)傳遞給我們已經(jīng)介紹過的 collect()
方法。實(shí)現(xiàn)自己的collector可以為您提供最大的靈活性,但也更具技術(shù)性。
了解collector的參數(shù)類型
讓我們檢查一下這個(gè)接口的參數(shù)。
interface Collector<T, A, R> {
// content of the interface
}
讓我們首先檢查以下類型:T
,R
第一種類型是 ,它對(duì)應(yīng)于此collector正在處理的流元素的類型。T
最后一個(gè)類型是 ,它是此collector生成的類型。R
比如在 Stream
實(shí)例上調(diào)用的 toList()
collector,類型R
為 List
。它 toSet()
collector將是 Set
。
groupingBy()
方法接收一個(gè)函數(shù)作參數(shù),來計(jì)算返回map的鍵。如果用它收集 Stream
,則需要傳遞一個(gè)對(duì)T
實(shí)例作mapping 的函數(shù)。因此,生成的map的類型將為 Map<K,List<T>>
。也就是R
的類型。
A
類型處理起來比較復(fù)雜。您可能已嘗試使用 IDE 來存儲(chǔ)您在前面的示例中創(chuàng)建的collector之一。如果這樣做,您可能意識(shí)到 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);
對(duì)于所有這些collector,第二個(gè)參數(shù)類型僅為 ?
。
如果需要實(shí)現(xiàn)Collector
接口,則必須為A
提供個(gè)顯式值。A
是此collector使用的中繼可變?nèi)萜鞯膶?shí)際類型。對(duì)于 toList(
) collector,它將是 ArrayList
,對(duì)于 toSet()
collector,它將是 HashSet
。事實(shí)上,此類型被 toList()
的返回類型隱藏了,這就是為什么在前面的示例中無法將 ?
類型替換為 ArrayList
的原因。
即使內(nèi)部可變?nèi)萜魇怯蓪?shí)現(xiàn)直接返回的,也可能發(fā)生類型A
和R
不同的情況。例如, toList()
,您可以通過修改 ArrayList
Collector><
T,A,R> 接口。
了解collector的特征
collector定義了內(nèi)部特征,流實(shí)現(xiàn)用它來優(yōu)化collector使用。
有三個(gè)。
-
IDENTITY_FINISH
指示此collector的finisher是恒等函數(shù)。該實(shí)現(xiàn)不會(huì)為具有此特征的collector調(diào)用finisher。 -
UNORDERED
指示此collector不保留它處理流元素的順序。toSet()
collector就是這種情況。而toList()
就沒有。 -
CONCURRENT
特性表示accumulator用來存儲(chǔ)已處理元素的容器支持并發(fā)訪問。這一點(diǎn)對(duì)于并行流很重要。
這些特征在collectorCollector.Characteristics
枚舉中定義,并由Collector
接口的 characteristics()
方法以set返回。
實(shí)現(xiàn) toList() 和 toSet() collector
使用這些元素,您現(xiàn)在可以重新創(chuàng)建類似于 toList()
collector的實(shí)現(xiàn)。
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);//不調(diào)用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);
此代碼打印出以下結(jié)果。
result = [one, two, three, four, five]
實(shí)現(xiàn)一個(gè)類似于 toSet()
的collector只需要兩處修改。
-
supplier()
方法將返回HashSet::new
-
characteristics()
方法會(huì)將Features.UNORDERED
添加到返回的set中。
實(shí)現(xiàn) joining() collector
重新創(chuàng)建此collector的實(shí)現(xiàn)很有趣,因?yàn)樗粚?duì)字符串進(jìn)行操作,并且它的finisher不是恒等函數(shù)。
此collector在 StringBuffer
實(shí)例中累積它處理的字符串,然后調(diào)用 toString()
方法以生成final結(jié)果。
此collector的特征集為空。它確實(shí)保留了處理元素的順序(因此沒有UNORDERED特征),它的finisher不是恒等函數(shù),并且不能并發(fā)使用。
讓我們看看如何實(shí)現(xiàn)這個(gè)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() {//會(huì)調(diào)用
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);
運(yùn)行此代碼將生成以下結(jié)果。
result = onetwothreefourfive
要支持分隔符、前綴和后綴可以使用 StringJoiner
。
使用Optional
創(chuàng)建Optional對(duì)象
Optional
類是具有私有構(gòu)造函數(shù)的final類。因此,創(chuàng)建它實(shí)例的唯一方法是調(diào)用其工廠方法之一。其中有三個(gè)。
- 您可以通過調(diào)用
Optional.empty()
創(chuàng)建一個(gè)空的Optional。 - 您可以通過調(diào)用
Optional.of()
將某元素作為參數(shù)。不允許將 null傳遞給此方法。這種情況下,您將獲得一個(gè) NullPointerException
。 - 您可以通過調(diào)用
Optional.ofNullable()
將某元素作為參數(shù)??梢詫ull傳遞給此方法。這種情況下,您將獲得一個(gè)空的Optional。
這些是創(chuàng)建此類實(shí)例的唯一方法。如您所見,不能將null直接賦給Optional對(duì)象。打開非空Optional將始終返回非null。
Optional<T>
有三個(gè)等效的類,用于專用數(shù)字流:OptionalInt
、OptionalLong
和 OptionalDouble
。這些類是原始類型(即值)的包裝器。ofNullable()
方法對(duì)這些類沒有意義,因?yàn)樵贾挡荒転閚ull。
打開Optional對(duì)象
有幾種方法可以使用Optional元素并訪問它包裝的元素(如果有)。你可以直接查詢你擁有的實(shí)例,如果里面有東西,就打開它,或者你可以在上面使用類似流的方法: map()
, flatMap()
, filter()
,甚至是 forEach()
的等價(jià)物。
打開Optional以獲取其內(nèi)容時(shí)應(yīng)謹(jǐn)慎,因?yàn)槿绻鸒ptional為空,它將引發(fā) NoSuchElementException
。除非您確定Optional元素中存在元素,否則應(yīng)首先通過測(cè)試來保護(hù)此操作。
有兩種方法可供您測(cè)試Optional對(duì)象:isPresent()
和 isEmpty(),
它們?cè)?Java SE 11 中添加。
然后,要打開您的Optional,您可以使用以下方法。
-
get()
:此方法已被棄用,因?yàn)?is 看起來像一個(gè) getter,但如果Optional為空,它可以拋出NoSuchElementException
。 -
orElseThrow()
是自 Java SE 10 以來的首選模式。它與get()
方法相同,但它的名稱毫無疑問它可以拋出NoSuchElementException
。 -
orElseThrow(Supplier exceptionSupplier):
與前面的方法相同。它使用您傳遞的supplier作為參數(shù)來創(chuàng)建它引發(fā)的異常。
您還可以提供一個(gè)對(duì)象,如果Optional對(duì)象為空,將返回該對(duì)象。
-
orElse(T returnObject):
如果在空的Optional值上調(diào)用,則返回參數(shù)。 -
orElseGet(Supplier supplier):
與前一個(gè)相同。實(shí)際上,僅在需要時(shí)調(diào)用所提供的supplier。
最后,如果此Optional為空,則可以創(chuàng)建另一個(gè)Optional。
-
or(supplier<Optional>supplier):
如果它不為空,則返回此未修改的Optional,如果空,則調(diào)用提供的supplier。此supplier創(chuàng)建另一個(gè)Optional供方法返回。
處理Optional對(duì)象
Optional
類還提供模式,以便您可以將Optional對(duì)象與流處理集成。它具有直接對(duì)應(yīng)Stream API 的方法,您可以使用這些方法以相同的方式處理數(shù)據(jù),并且將與流無縫集成。這些方法是 map()
, filter()
,和flatMap()
,前兩個(gè)接收的參數(shù)與Stream API中的方法相同,后者的函數(shù)參數(shù)需要返回Optional<T>
而不是Stream
。
這些方法按以下規(guī)則返回Optional對(duì)象。
- 如果調(diào)用的對(duì)象為空,則返回Optional。
- 如果不為空,則它們的參數(shù)、函數(shù)或Predicate將應(yīng)用于此Optional的內(nèi)容。將結(jié)果包裝在另一個(gè)Optional中返回。
使用這些方法可以在某些流模式中生成更具可讀性的代碼。
假設(shè)您有一個(gè)具有id
屬性的Customer
實(shí)例列表。您需要查找具有給定 ID 的客戶的名稱。
您可以使用以下模式執(zhí)行此操作。
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對(duì)象是否為空;調(diào)用map()
實(shí)際上可以為您執(zhí)行此操作。
找出發(fā)表文章最多的兩位聯(lián)合作者
讓我們看另一個(gè)更復(fù)雜的示例。通過此示例,向您展示Stream API、Collector API 和Optional對(duì)象的幾種主要模式。
假設(shè)您有一組需要處理的文章。一篇文章有標(biāo)題、發(fā)表年份和作者列表。作者有一個(gè)名字。
您的列表中有很多文章,您需要知道哪些作者一起聯(lián)合發(fā)表了最多的文章。
您的第一個(gè)想法可能是為文章構(gòu)建一對(duì)作者的流。這實(shí)際上是文章和作者集的笛卡爾乘積。您并不需要此流中的所有對(duì)。您對(duì)兩位作者實(shí)際上是同一對(duì)的情況不感興趣;一對(duì)作者(A1,A2)與(A2,A1)實(shí)際相同。若要實(shí)現(xiàn)此約束,可以添加約束條件,聲明一對(duì)作者時(shí),聲明作者按字母順序排序。
讓我們?yōu)檫@個(gè)模型寫兩條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實(shí)現(xiàn)了排序后的創(chuàng)建
if (first.compareTo(second) > 0) {
return Optional.of(new PairOfAuthors(first, second));
} else {
return Optional.empty();
}
}
}
在PairOfAuthors
record中的創(chuàng)建工廠方法,可以控制哪些實(shí)例是允許的,并防止不需要的創(chuàng)建。若要表明此工廠方法可能無法生成結(jié)果,可以將其包裝在Optional方法中。這完全尊重了以下原則:如果無法生成結(jié)果,則返回一個(gè)空的 optional。
讓我們編寫一個(gè)函數(shù),為給定的文章創(chuàng)建一個(gè) Stream<PairOfAuthors>
。您可以用兩個(gè)嵌套流生成笛卡爾乘積。
作為第一步,您可以編寫一個(gè)bifunction,從文章和作者創(chuàng)建此流。
BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors =
(article, firstAuthor) ->
article.authors().stream().flatMap(//對(duì)每個(gè)author都遍歷authors創(chuàng)建作者對(duì),生成Stream<PairOfAuthors>
secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());//Optional的Stream
此bifunction從 firstAuthor
和secondAuthor
創(chuàng)建一個(gè)Optional對(duì)象,取自基于文章作者構(gòu)建的流。您可以看到 stream(
) 方法是在 of()
方法返回的Optional對(duì)象上調(diào)用的。如果Optional流為空,則返回的流為空,否則僅包含一對(duì)作者。此流由 flatMap()
方法處理。此方法打開流,空的流將消失,并且只有有效的對(duì)將出現(xiàn)在生成的流中。
您現(xiàn)在可以構(gòu)建一個(gè)函數(shù),該函數(shù)使用此bifunction從文章中創(chuàng)建作者對(duì)流。
Function<Article, Stream<PairOfAuthors>> toPairOfAuthors =
article ->
article.authors().stream()
.flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));
找到聯(lián)合發(fā)表最多的兩位作者可以通過柱狀圖來完成,柱狀圖中的鍵是作者對(duì),值是他們一起寫的文章數(shù)。
您可以使用 groupingBy()
構(gòu)建柱狀圖。讓我們首先創(chuàng)建一對(duì)作者的流。
Stream<PairOfAuthors> pairsOfAuthors =
articles.stream()
.flatMap(toPairOfAuthors);
此流的構(gòu)建方式是,如果一對(duì)作者一起寫了兩篇文章,則這對(duì)作者在流中出現(xiàn)兩次。因此,您需要做的是計(jì)算每個(gè)對(duì)在此流中出現(xiàn)的次數(shù)。這可以通過 groupingBy()
來完成,其中分類器是恒等函數(shù):對(duì)本身。此時(shí),這些值是您需要計(jì)數(shù)的對(duì)列表。所以下游collector只是 counting()
collector。
Map<PairOfAuthors, Long> numberOfAuthorsTogether =
articles.stream()
.flatMap(toPairOfAuthors)//所有文章的Stream<PairOfAuthors>
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()//<PairOfAuthors, Long>
));
找到一起發(fā)表文章最多的作者包括提取此map的最大值。您可以為此處理創(chuàng)建以下函數(shù)。
Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor =
map -> map.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow();
此函數(shù)在 Stream.max()
方法返回的Optional對(duì)象上調(diào)用 orElseThrow()
方法。
這個(gè)Optional對(duì)象可以為空嗎?要使其為空,map本身必須為空,這意味著原始流中沒有成對(duì)的作者。只要您至少有一篇文章至少有兩位作者,那么這個(gè)Optional就不為空。
找出每年發(fā)表文章最多的兩位聯(lián)合作者
讓我們更進(jìn)一步,想知道您是否可以根據(jù)年份進(jìn)行相同的處理。事實(shí)上,如果能使用單個(gè)collector實(shí)現(xiàn),接下來就可以將其作為下游collector傳遞給 groupingBy(Article::inceptionYear)
。
對(duì)map后續(xù)提取最大值可以使用collectingAndThen()
。此模式已在上一節(jié)“使用finisher對(duì)collector進(jìn)行后續(xù)處理”中介紹過。此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();
現(xiàn)在,您可以將它們合并到單個(gè) collectingAndThen()
中。將 groupingBy()
作為為第一個(gè)參數(shù),將finisher
作為第二個(gè)。
Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector =
Collectors.collectingAndThen(
Collectors.groupingBy(
Function.identity(),
Collectors.counting()
),
map -> map.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow()
);
現(xiàn)在,您可以使用初始flatmap操作和此collector編寫完整模式。
Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
articles.stream()
.flatMap(toPairOfAuthors)
.collect(pairOfAuthorsEntryCollector);
多虧了 flatMapping
(),您可以通過合并中繼 flatMap()
和末端collector來使用單個(gè)collector編寫此代碼。以下代碼等效于上一個(gè)代碼。
Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
articles.stream()
.collect(
Collectors.flatMapping(
toPairOfAuthors,
pairOfAuthorsEntryCollector));
找到每年發(fā)表最多的兩位聯(lián)合作者,只需將這個(gè) 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
)
);
你可能還記得,在這個(gè)flatMapping
()的深處,有一個(gè)對(duì)Optional.orElseThrow()
的調(diào)用。在這個(gè)的模式中,很容易檢查此調(diào)用是否會(huì)失敗,因?yàn)榇藭r(shí)有一個(gè)空的Optional很容易猜到。
現(xiàn)在我們已將此collector用作下游collector,情況就不同了。你怎么能確定,每年至少有一篇文章由至少兩位作者撰寫?保護(hù)此代碼免受任何 NoSuchElementException
的影響會(huì)更安全。
避免打開Optional
在第一個(gè)上下文中可以接受的模式現(xiàn)在更加危險(xiǎn)。處理它包括首先不要調(diào)用orElseThrow()。
這種情況下,collector將變?yōu)橐韵马?xiàng)。它不是創(chuàng)建一對(duì)作者和一長(zhǎng)串?dāng)?shù)字的鍵值對(duì),而是將結(jié)果包裝在一個(gè)Optional對(duì)象中。
Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>>
pairOfAuthorsEntryCollector =
Collectors.collectingAndThen(
Collectors.groupingBy(
Function.identity(),
Collectors.counting()
),
map -> map.entrySet().stream()
.max(Map.Entry.comparingByValue())
);
請(qǐng)注意,orElseThrow()
不再被調(diào)用,從而導(dǎo)致collector的簽名中有一個(gè)Optional。
這個(gè)Optional也出現(xiàn)在 flatMapping()
collector的簽名中。
Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =
Collectors.flatMapping(
toPairOfAuthors,
pairOfAuthorsEntryCollector
);
使用此collector會(huì)創(chuàng)建一個(gè)類型為 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>
類型的map,我們不需要這種類型:擁有一個(gè)值為Optional的map是無用的,而且可能很昂貴。這是一種反模式。不幸的是,在計(jì)算此最大值之前,您無法猜測(cè)此Optional是否為空。
構(gòu)建此中繼map后,您需要?jiǎng)h除空的Optional來構(gòu)建表示所需柱狀圖的map。我們將使用與之前相同的技術(shù):在flatMap() 中調(diào)用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為空,會(huì)成為空流,從而安全跳過
.map(value -> Map.entry(entry.getKey(), value))
.stream())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue
)); // Map<Integer, Map.Entry<PairOfAuthors, Long>>
請(qǐng)注意此模式中的flatmap函數(shù)。它接受一個(gè)entry
作參數(shù) ,類型為 Optional<Map.Entry<PairOfAuthors, Long>>
,并在此Optional上調(diào)用 map()。
如果Optional為空,則此調(diào)用返回空的Optional。然后忽略map函數(shù)。接下來調(diào)用 stream(
) 返回一個(gè)空流,該流將從主流中刪除,因?yàn)槲覀兲幱?flatMap()
調(diào)用中。
如果Optional中有一個(gè)值,則使用此值調(diào)用map函數(shù)。此map函數(shù)創(chuàng)建一個(gè)具有相同鍵和此現(xiàn)有值的新鍵值對(duì)。此鍵值對(duì)的類型為 Map.Entry,
并且通過此 map()
方法將其包裝在Optional對(duì)象中。對(duì) stream(
) 的調(diào)用會(huì)創(chuàng)建一個(gè)包含此Optional內(nèi)容的流,然后由 flatMap()
調(diào)用打開該流。
此模式用空的Optional將 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>>
mapping為 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>
,刪除所有具有空Optional的鍵/值對(duì)。
使用 toMap()
collector可以安全地重新創(chuàng)建map,因?yàn)槟涝诖肆髦胁荒苁褂脙纱蜗嗤逆I。
此模式使用了Optional和Stream API 的三個(gè)要點(diǎn)。
-
Optional.map()
方法,如果在空Optional上調(diào)用,則返回空的Optional。 -
Optional.stream()
方法,該方法在 Optional的內(nèi)容上打開流。如果Optional為空,則返回的流也為空。它允許您從Optional空間無縫的移動(dòng)到流空間。 -
Stream.flatMap()
方法,用于打開從Optional構(gòu)建的流,以靜默方式刪除空流。
消費(fèi)Optional的內(nèi)容
Optional
類還具有兩個(gè)將Consumer作為參數(shù)的方法。
-
ifPresent(Consumer consumer)
:此方法使用此Optional的內(nèi)容(如果有)調(diào)用提供的Consumer。它實(shí)際上等同于Stream.forEach(Consumer)
方法。 -
ifPresentOrElse(Consumer consumer, Runnable runnable):
如果 Optional非空,此方法與前一個(gè)方法相同。如果空,則調(diào)用提供的Runnable
實(shí)例。
燒哥總結(jié)
(驗(yàn)證中,代碼庫(kù)持續(xù)更新)
lambda將匿名類換成了匿名方法,能代表某個(gè)操作,讓代碼更直觀(語法糖),但良好的命名
很重要。
改寫為lambda首先得是函數(shù)接口,Operator是Function的簡(jiǎn)化版。
可以序列化,從而可以作為字段、方法參數(shù)和返回類型,實(shí)現(xiàn)了方法引用
、鏈?zhǔn)秸{(diào)用、函數(shù)式編程。
lambda已經(jīng)深入JDK內(nèi)部,所以性能方面很關(guān)注,為避免裝箱拆箱,提供了很多原生類型專用版,但有時(shí)候要手動(dòng)裝箱。
為了性能
,避免在內(nèi)存中處理大量數(shù)據(jù),同時(shí)也提高可讀性,出現(xiàn)了Stream API。
流處理的整個(gè)過程最好都是流,所以有flatmap、mapMulti各種中繼操作,
甚至末端collector也可以有下游collector,甚至collector可以串聯(lián)、三通,比如神奇的Collectors.flatMapping()
流不應(yīng)該作為變量或參數(shù)。
流中不應(yīng)該改變外圍變量,會(huì)捕獲外界變量,降低處理性能
,也會(huì)把并行流變成多線程并發(fā)。
每次中繼操作都產(chǎn)生一個(gè)新流。
同樣為了性能
,reduce可以并行,但要具有可結(jié)合性
、要有幺元
。如果幺元未知,會(huì)返回Optional。
三參數(shù)的reduce組合了mapping過程。
專用數(shù)字流
的sum、min、max、count、average、summaryStatistics為末端操作。
轉(zhuǎn)換為流的源如果用Set,會(huì)是亂序的。
map、flatmap會(huì)刪除SORTED、DISTINCTED、NONNULL。
本教程未詳細(xì)說明的:spliterator、不可變流、并發(fā)流。
Stream.collect
(Collectors.toList()) 只能用于對(duì)象流,數(shù)字流要么裝箱,要么用三參數(shù)那個(gè),或者自定義collector,五個(gè)參數(shù)。
flatmap會(huì)跳過空流,包括Optional.stream()
產(chǎn)生的流,所以看到Optional,不要orElseThrow()
,可以用flatmap取出。
API是看起來越來越復(fù)雜,Collectors.mapping()
public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper,
Collector<? super U,A,R> downstream)
方法名前面四個(gè),返回類型里三個(gè),還有問號(hào),參數(shù)里super了三個(gè)。文章來源:http://www.zghlxwxcb.cn/news/detail-522858.html
Map<City, Set<String>> lastNamesByCity
= people.stream().collect(
groupingBy(Person::getCity,
mapping(Person::getLastName,
toSet())));
雖然很好用。文章來源地址http://www.zghlxwxcb.cn/news/detail-522858.html
到了這里,關(guān)于從頭學(xué)Java17-Stream API(二)結(jié)合Record、Optional的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!