從頭學(xué)Java17-Stream API(一)
Stream API
Stream API 是按照map/filter/reduce方法處理內(nèi)存中數(shù)據(jù)的最佳工具。
本系列中的教程包含從基本概念一直到collector設(shè)計(jì)和并行流。

在流上添加中繼操作
將一個(gè)流map為另一個(gè)流
mapping流就是使用函數(shù)轉(zhuǎn)換其元素。此轉(zhuǎn)換可能會更改該流處理的元素的類型。
您可以使用 map() 方法將一個(gè)流map為另一個(gè)流,該方法用Function作為參數(shù)。mapping一個(gè)流意味著該流的所有元素都將使用該函數(shù)進(jìn)行轉(zhuǎn)換。
代碼模式如下:
List<String> strings = List.of("one", "two", "three", "four");
Function<String, Integer> toLength = String::length;
Stream<Integer> ints = strings.stream()
.map(toLength);
此代碼粘貼到 IDE 運(yùn)行時(shí),你不會看到任何東西,你可能想知道為什么。
答案其實(shí)很簡單:該流上沒有定義末端操作。這段代碼沒有做任何事情。它不處理任何數(shù)據(jù)。
讓我們添加一個(gè)非常有用的末端操作collect(Collectors.toList()),它將處理后的元素放在一個(gè)列表中。如果您不確定此代碼的真正作用,請不要擔(dān)心;我們將在本教程的后面部分介紹這一點(diǎn)。代碼將變?yōu)橐韵聝?nèi)容。
List<String> strings = List.of("one", "two", "three", "four");
List<Integer> lengths = strings.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("lengths = " + lengths);
運(yùn)行此代碼將打印以下內(nèi)容:
lengths = [3, 3, 5, 4]
您可以看到此模式創(chuàng)建了一個(gè) Stream<Integer>,由 map(String::length) 返回。你也可以通過調(diào)用mapToInt()來使其成為一個(gè)專門的IntStream。這個(gè)mapToInt()方法以ToIntFuction作參數(shù)。在上一示例中.map(String::length)更改為.mapToInt(String::length) 不會創(chuàng)建編譯器錯(cuò)誤。String::length方法引用可以是兩種類型:Function<String、Integer> 和 ToIntFunction<String>。
專用流沒有 collect() 方法將Collector作參數(shù)。因此,如果用 mapToInt(),則無法再在列表中收集結(jié)果。讓我們獲取有關(guān)該流的一些統(tǒng)計(jì)信息。這個(gè) summaryStatistics() 方法非常方便,并且僅在專門的原始類型流上可用。
List<String> strings = List.of("one", "two", "three", "four");
IntSummaryStatistics stats = strings.stream()
.mapToInt(String::length)
.summaryStatistics();
System.out.println("stats = " + stats);
結(jié)果如下:
stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}
從 Stream 轉(zhuǎn)為原始類型流有三種方法:mapToInt()、mapToLong() 和 mapToDouble()。
filter流
filtering就是在流處理中使用Predicate丟棄某些元素。此方法可用于對象流和原始類型流。
假設(shè)您需要計(jì)算長度為 3 的字符串。您可以編寫以下代碼來執(zhí)行此操作:
List<String> strings = List.of("one", "two", "three", "four");
long count = strings.stream()
.map(String::length)
.filter(length -> length == 3)
.count();
System.out.println("count = " + count);
運(yùn)行此代碼將生成以下內(nèi)容:
count = 2
請注意,您剛剛使用了 Stream API 的另一個(gè)末端操作 count(),它只計(jì)算已處理元素的數(shù)量。此方法返回long ,您可以使用它計(jì)算很多元素。比 ArrayList 里面的更多。
flatmap流以處理 1:p 關(guān)系
讓我們在一個(gè)示例中查看 flatMap 操作。假設(shè)您有兩個(gè)實(shí)體:State和 City。一個(gè)state實(shí)例包含多個(gè)city實(shí)例,存儲在一個(gè)列表中。
這是City類的代碼。
public class City {
private String name;
private int population;
// constructors, getters
// toString, equals and hashCode
}
這是State類的代碼,以及與City類的關(guān)系。
public class State {
private String name;
private List<City> cities;
// constructors, getters
// toString, equals and hashCode
}
假設(shè)您的代碼正在處理狀態(tài)列表,并且在某些時(shí)候您需要計(jì)算所有城市的人口。
您可以編寫以下代碼:
List<State> states = ...;
int totalPopulation = 0;
for (State state: states) {
for (City city: state.getCities()) {
totalPopulation += city.getPopulation();
}
}
System.out.println("Total population = " + totalPopulation);
此代碼的內(nèi)部循環(huán)是 map-reduce 的一種形式,也可以使用流編寫:
totalPopulation += state.getCities().stream().mapToInt(City::getPopulation).sum();
外層和內(nèi)層有點(diǎn)不匹配,將流放入循環(huán)中不是一個(gè)很好的代碼模式。
這正是flatmap的作用。此運(yùn)算符在對象之間打開一對多關(guān)系,并基于這些關(guān)系創(chuàng)建流。flatMap() 方法將一個(gè)特殊函數(shù)作為參數(shù),這個(gè)函數(shù)返回 Stream 對象。類與類之間的關(guān)系由此函數(shù)定義。
在我們的示例中,此函數(shù)很簡單,因?yàn)?code>State類中有一個(gè)List<City>。所以你可以按以下方式編寫它。
//根據(jù)state和city的關(guān)系,生成city流
Function<State, Stream<City>> stateToCity = state -> state.getCities().stream();
此List類型不是強(qiáng)制的。假設(shè)您有一個(gè)包含 Map <String,Country>的Continent類,其中鍵是國家/地區(qū)的代碼(CAN 表示加拿大,MEX 表示墨西哥,F(xiàn)RA 表示法國等)。假設(shè)該類有一個(gè)返回此map的方法getCountries()。
這種情況下,可以通過這種方式編寫此函數(shù)。
Function<Continent, Stream<Country>> continentToCountry =
continent -> continent.getCountries().values().stream();
flatMap() 方法的處理分兩個(gè)步驟。
- 第一步,使用此函數(shù)mapping流的所有元素。從
Stream<State>創(chuàng)建一個(gè)Stream<Stream<City>>,每個(gè)州都map為城市流。 - 第二步,展平產(chǎn)生的流。您最終會得到一個(gè)單一的流,其中包含所有州的所有城市。
因此,使用flatmap,之前的嵌套 for 編寫的代碼可以改寫為:
List<State> states = ...;
int totalPopulation =
states.stream()
.flatMap(state -> state.getCities().stream())//對每個(gè)state,都轉(zhuǎn)換為city流,最后合并
.mapToInt(City::getPopulation)
.sum();
System.out.println("Total population = " + totalPopulation);
使用flatmap和 MapMulti 驗(yàn)證元素轉(zhuǎn)換
flatMap 可用于流元素的轉(zhuǎn)換中的驗(yàn)證。
假設(shè)您有一個(gè)表示整數(shù)的字符串流。您需要用 Integer.parseInt() 將它們轉(zhuǎn)為整數(shù)。不幸的是,其中一些字符串有問題:也許有些字符串為空,null,或者末尾有額外的空白字符。這些都會使解析失敗,并出現(xiàn) NumberFormatException。當(dāng)然,您可以嘗試filter此流,用Predicate刪除錯(cuò)誤的字符串,但最安全的方法是使用 try-catch 模式。
如下所示。
Predicate<String> isANumber = s -> {
try {
int i = Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
};
第一個(gè)缺陷是您需要實(shí)際進(jìn)行轉(zhuǎn)換以查看它是否有效。然后,您不得不在mapping函數(shù)中再次執(zhí)行此操作:不要這樣做!第二個(gè)缺陷是,從catch塊return,絕不是一個(gè)好主意。
您真正需要做的是,當(dāng)此字符串中有一個(gè)正確的整數(shù)時(shí)返回一個(gè)整數(shù),如果有問題,則什么都不返回。這是flatmap的工作。如果可以解析整數(shù),則可以返回包含結(jié)果的流。另一種情況下,您可以返回空流。
然后,可以編寫以下函數(shù)。
Function<String, Stream<Integer>> flatParser = s -> {//根據(jù)String與Integer的關(guān)系,生成Integer流
try {
return Stream.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
}
return Stream.empty();
};
List<String> strings = List.of("1", " ", "2", "3 ", "", "3");
List<Integer> ints =
strings.stream()
.flatMap(flatParser)//對每個(gè)String,都轉(zhuǎn)為Integer流,最后合并 flatmap會跳過空流
.collect(Collectors.toList());
System.out.println("ints = " + ints);
運(yùn)行此代碼將生成以下結(jié)果。所有有問題的字符串都已靜默刪除。
ints = [1, 2, 3]
這種flatmap代碼的使用效果很好,但它有一個(gè)開銷:為流的每個(gè)元素都會創(chuàng)建一個(gè)流。從 Java SE 16 開始,Stream API 中添加了一個(gè)方法:當(dāng)您創(chuàng)建零個(gè)或一個(gè)對象的多個(gè)流時(shí)。此方法稱為mapMulti(),并將BiConsumer作為參數(shù)。
此 BiConsumer 使用兩個(gè)參數(shù):
- 需要mapping的流元素
- 對mapping結(jié)果調(diào)用的
Consumer
調(diào)用Consumer會將該元素添加到生成的流中。如果mapping無法完成,則biconsumer不會調(diào)用此消費(fèi)者,并且不會添加任何元素。
讓我們用這個(gè) mapMulti() 方法重寫你的模式。
List<Integer> ints =
strings.stream()
.<Integer>mapMulti((string, consumer) -> {//方法前面聲明Integer
try {
consumer.accept(Integer.parseInt(string));//直接說明跟Integer的關(guān)系,生成最終Integer流
} catch (NumberFormatException ignored) {
}
})
.collect(Collectors.toList());
System.out.println("ints = " + ints);
運(yùn)行此代碼會產(chǎn)生與以前相同的結(jié)果。所有有問題的字符串都已被靜默刪除,但這一次,沒有創(chuàng)建其他流。
ints = [1, 2, 3]
使用此方法,需要告訴編譯器 Consumer 的類型。通過這種特殊語法,在 mapMulti() 前聲明此類型。它不是您在 Java 代碼中經(jīng)常看到的語法。您可以在靜態(tài)和非靜態(tài)上下文中使用它。
刪除重復(fù)項(xiàng)并對流進(jìn)行排序
Stream API 有兩個(gè)方法,distinct() 和 sorted(),去重和排序。distinct() 方法使用 hashCode() 和 equals() 方法來發(fā)現(xiàn)重復(fù)項(xiàng)。sorted() 方法有一個(gè)重載,需要一個(gè)comparator,用于比較和排序。如果未提供,則假定流元素具有可比性。否則,則會引發(fā) ClassCastException。
您可能還記得本教程的前一部分,流應(yīng)該是不存儲任何數(shù)據(jù)的空對象。此規(guī)則也有例外,這兩個(gè)方法就是。
事實(shí)上,為了發(fā)現(xiàn)重復(fù)項(xiàng),distinct() 方法需要存儲流元素。當(dāng)處理一個(gè)元素時(shí),首先檢查該元素是否見到過。
sorted() 也是如此。此方法需要存儲所有元素,然后在內(nèi)部緩沖區(qū)中對它們進(jìn)行排序,再發(fā)送到管道的下一步。
distinct() 可以用于非綁定(無限)流,而 sorted() 不能。
限制和跳過流的元素
Stream API 提供了兩種選擇流元素的方法:基于索引或使用Predicate。
第一種方法,使用 skip() 和 limit() 方法,兩者都將 long 作為參數(shù)。使用這些方法時(shí),需要避免一個(gè)小陷阱。您需要記住,每次在流中調(diào)用中繼方法時(shí),都會創(chuàng)建一個(gè)新流。因此,如果您在 skip() 之后調(diào)用 limit(),請不要忘記從該新流開始計(jì)算。
假設(shè)您有一個(gè)包含所有整數(shù)的流,從 1 開始。您需要選擇 3 到 8 之間的整數(shù)。正確的代碼如下。
List<Integer> ints = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> result =
ints.stream()
.skip(2)//產(chǎn)生了新流
.limit(5)//不是limit(8)
.collect(Collectors.toList());
System.out.println("result = " + result);
此代碼打印以下內(nèi)容。
result = [3, 4, 5, 6, 7]
Java SE 9 又引入了兩種方法。它不是根據(jù)元素在流中的索引跳過和限制元素,而是根據(jù)Predicate。
dropWhile(Predicate)如果Predicate為true,一直跳過元素,直到Predicate為false。此時(shí),該流后面所有元素都將傳輸?shù)较乱粋€(gè)流。takeWhile(Predicate)做相反的事情:如果Predicate為true,它一直將元素傳輸?shù)较乱粋€(gè)流,直到Predicate為false,后面都跳過。這個(gè)是短路的
請注意,這些方法的工作方式類似于門。一旦 dropWhile() 打開了門讓處理后的元素流動,它就不會關(guān)閉它。一旦 takeWhile() 關(guān)閉了門,它就不能重新打開它,沒有更多的元素將被發(fā)送到下一個(gè)操作。
串聯(lián)流
Stream API 提供了多種模式,可將多個(gè)流連接成一個(gè)。最明顯的方法是使用 Stream 接口中定義的工廠方法:concat()。
此方法接收兩個(gè)流并生成一個(gè)流,其中包含第一個(gè)流的元素,然后是第二個(gè)流的元素。
您可能想知道為什么此方法不用 vararg 來連接任意數(shù)量的流。如果你有兩個(gè)以上,JavaDoc API文檔建議你使用另一種模式,基于flatmap。
讓我們在一個(gè)例子上看看這是如何工作的。
List<Integer> list0 = List.of(1, 2, 3);
List<Integer> list1 = List.of(4, 5, 6);
List<Integer> list2 = List.of(7, 8, 9);
// 1st pattern: concat
List<Integer> concat =
Stream.concat(list0.stream(), list1.stream())
.collect(Collectors.toList());
// 2nd pattern: flatMap
List<Integer> flatMap =
Stream.of(list0.stream(), list1.stream(), list2.stream())//類似city的外層組成的流
.flatMap(Function.identity())//變成了city流,每個(gè)Stream<Integer>要變成Stream<Integer>,原樣返回即可
.collect(Collectors.toList());
System.out.println("concat = " + concat);
System.out.println("flatMap = " + flatMap);
運(yùn)行此代碼將產(chǎn)生以下結(jié)果:
concat = [1, 2, 3, 4, 5, 6]
flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9]
建議使用 flatMap() 方式的原因是 concat() 在連接期間會創(chuàng)建中繼流,來連接兩個(gè)流。如果需要連接多個(gè),則最終每次串聯(lián)都需要一個(gè)很快就會被丟棄的流。
使用flatmap模式,您只需創(chuàng)建一個(gè)流來保存所有流并執(zhí)行flatmap。開銷要低得多。
您可能想知道為什么添加了這兩種模式??雌饋?concat() 并不是很有用。事實(shí)上,如果連接的兩個(gè)流的源的大小已知,則生成的流的大小也是已知的,只是兩個(gè)串聯(lián)流的總和。
如果連接的兩個(gè)流的源的大小已知,則生成的流的大小也是已知的。實(shí)際上,它只是兩個(gè)串聯(lián)流的總和。
在流上使用flatmap可能會創(chuàng)建未知數(shù)量的元素,以便在生成的流中進(jìn)行處理。Stream API 會丟失對元素數(shù)量的跟蹤。
換句話說:concat 產(chǎn)生一個(gè) SIZED 流,而flatmap不會。此 SIZED 屬性是流可能具有的一種屬性,本教程稍后將介紹。
調(diào)試流
有時(shí),在運(yùn)行時(shí)能檢查流處理的元素可能很方便。Stream API 有一個(gè)方法:peek() 方法。此方法用于調(diào)試數(shù)據(jù)處理管道。不應(yīng)在生產(chǎn)代碼中使用此方法。
絕對不要使用此方法在應(yīng)用程序中執(zhí)行一些副作用。
此方法將Consumer作為參數(shù),將每個(gè)元素上調(diào)用。讓我們實(shí)際效果。
List<String> strings = List.of("one", "two", "three", "four");
List<String> result =
strings.stream()
.peek(s -> System.out.println("Starting with = " + s))
.filter(s -> s.startsWith("t"))
.peek(s -> System.out.println("Filtered = " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("Mapped = " + s))
.collect(Collectors.toList());
System.out.println("result = " + result);
如果運(yùn)行此代碼,您將在控制臺上看到以下內(nèi)容。
Starting with = one
Starting with = two
Filtered = two
Mapped = TWO
Starting with = three
Filtered = three
Mapped = THREE
Starting with = four
result = [TWO, THREE]
讓我們分析一下這個(gè)輸出。
- 要處理的第一個(gè)元素是
one。你可以看到它被filter掉了。 - 第二個(gè)是
two。此元素通過filter,然后map為大寫。然后將其添加到結(jié)果列表中。 - 第三個(gè)是
three,它也通過filter。 - 最后一個(gè)是
four,被filtering步驟拒絕。
有一點(diǎn)你在本教程前面看到,現(xiàn)在很明顯:流確實(shí)一一處理了它必須處理的所有元素,從流的開始到結(jié)束。這在之前已經(jīng)提到過,現(xiàn)在你可以看到它的實(shí)際效果。
您可以看到,此peek(System.out::println)模式對于逐個(gè)跟蹤流處理的元素非常有用,無需調(diào)試代碼。調(diào)試流很困難,需要小心放置斷點(diǎn)的位置。大多數(shù)情況下,在流處理上放置斷點(diǎn)會跳轉(zhuǎn)到Stream接口的實(shí)現(xiàn)。這不是你需要的。您需要將這些斷點(diǎn)放在 lambda 表達(dá)式的代碼中。
創(chuàng)建流
創(chuàng)建流
在本教程中,您已經(jīng)創(chuàng)建了許多流,所有這些都是通過調(diào)用 Collection 接口的 stream() 方法創(chuàng)建的。此方法非常方便:只需要兩行簡單的代碼,您可以使用此流來試驗(yàn)Stream API 的幾乎任何功能。
如您所見,還有許多其他方法。了解這些方法后,您可以在應(yīng)用程序中的許多位置利用 Stream API,并編寫更具可讀性和可維護(hù)性的代碼。
讓我們快速瀏覽您將在本教程中看到的內(nèi)容,然后再深入研究它們中的每一個(gè)。
第一組模式使用 Stream 接口中的工廠方法。使用它們,您可以從以下元素創(chuàng)建流:
- vararg 參數(shù);
- supplier;
- unary operator,從前一個(gè)元素生成下一個(gè)元素;
- builder。
您甚至可以創(chuàng)建空流,這在某些情況下可能很方便。
您已經(jīng)看到可以在集合上創(chuàng)建流。如果您擁有的只是一個(gè)iterator,而不是一個(gè)成熟的集合,那么您可以在iterator上創(chuàng)建流。如果你有一個(gè)數(shù)組,那么還有一個(gè)模式可以在數(shù)組的元素上創(chuàng)建一個(gè)流。
它并不止于此。JDK 中的許多模式也已添加到眾所周知的對象中。然后,您可以從以下元素創(chuàng)建流:
- 字符串的字符;
- 文本文件的行;
- 通過使用正則表達(dá)式拆分字符串來創(chuàng)建的元素;
- 一個(gè)隨機(jī)變量,可以創(chuàng)建隨機(jī)數(shù)流。
您還可以使用builder模式創(chuàng)建流。
從集合或iterator創(chuàng)建流
您已經(jīng)知道Collection接口中有一個(gè)可用的 stream() 。這可能是創(chuàng)建流的最經(jīng)典方法。
在某些情況下,您可能需要基于map的內(nèi)容創(chuàng)建流。Map 接口中沒有stream()方法,因此無法直接創(chuàng)建。但是,您可以通過三個(gè)集合訪問map的內(nèi)容:
- 鍵的集合,
keySet() - 鍵值對的集合,
entrySet() - 值的集合,
values ()。
Stream API 提供了一種從簡單iterator創(chuàng)建流的模式,它可能是在非標(biāo)準(zhǔn)數(shù)據(jù)源上創(chuàng)建流的非常方便的方法。模式如下。
Iterator<String> iterator = ...;
long estimateSize = 10L;
int characteristics = 0;
Spliterator<String> spliterator = Spliterators.spliterator(iterator, estimateSize, characteristics);
boolean parallel = false;
Stream<String> stream = StreamSupport.stream(spliterator, parallel);
此模式包含幾個(gè)神奇元素,本教程稍后將介紹。讓我們快速瀏覽它們。
estimateSize是您認(rèn)為此流將消費(fèi)的元素?cái)?shù)。在某些情況下,此信息很容易獲得:例如,如果要在數(shù)組或集合上創(chuàng)建流。但某些情況下是未知的。
本教程稍后將介紹characteristics參數(shù)。它用于優(yōu)化數(shù)據(jù)的處理。
parallel參數(shù)告知 API 要?jiǎng)?chuàng)建的流是否為并行流。本教程稍后將介紹。
創(chuàng)建空流
讓我們從最簡單的開始:創(chuàng)建一個(gè)空流。Stream接口中有一個(gè)工廠方法。您可以通過以下方式使用它。
Stream<String> empty = Stream.empty();
List<String> strings = empty.collect(Collectors.toList());
System.out.println("strings = " + strings);
運(yùn)行此代碼會在主機(jī)上顯示以下內(nèi)容。
strings = []
在某些情況下,創(chuàng)建空流可能非常方便。事實(shí)上,您在本教程的前一部分看到了一個(gè),使用空流和flatmap從流中刪除無效元素。從 Java SE 16 開始,此模式已被 mapMulti() 模式所取代。
從 vararg 或數(shù)組創(chuàng)建流
兩種模式非常相似。第一個(gè)在 Stream 接口中使用 of() 工廠方法。第二個(gè)使用 Arrays 工廠類的 stream() 工廠方法。事實(shí)上,如果你檢查 Stream.of() 方法的源代碼,你會看到它調(diào)用了 Arrays.stream()。
這是第一個(gè)實(shí)際模式。
Stream<Integer> intStream = Stream.of(1, 2, 3);
List<Integer> ints = intStream.collect(Collectors.toList());
System.out.println("ints = " + ints);
運(yùn)行第一個(gè)示例將提供以下內(nèi)容:
ints = [1, 2, 3]
這是第二個(gè)。
String[] stringArray = {"one", "two", "three"};
Stream<String> stringStream = Arrays.stream(stringArray);
List<String> strings = stringStream.collect(Collectors.toList());
System.out.println("strings = " + strings);
運(yùn)行第二個(gè)示例將提供以下內(nèi)容:
strings = [one, two, three]
從supplier創(chuàng)建流
Stream 接口上有兩種工廠方法。
第一個(gè)是 generate(),以supplier為參數(shù)。每次需要新元素時(shí),都會調(diào)用該supplier。
您可以使用以下代碼創(chuàng)建這樣的流,但不要這樣做!
Stream<String> generated = Stream.generate(() -> "+");
List<String> strings = generated.collect(Collectors.toList());
如果你運(yùn)行這段代碼,你會發(fā)現(xiàn)它永遠(yuǎn)不會停止。如果您這樣做并且有足夠的耐心,您可能會看到 OutOfMemoryError。如果沒有,最好通過 IDE 終止應(yīng)用程序。它真的產(chǎn)生了無限的流。
我們還沒有介紹這一點(diǎn),但擁有這樣的流是完全合法的!您可能想知道它們有什么用?事實(shí)上有很多。要使用它們,您需要在某個(gè)時(shí)候截?cái)啻肆?,而Stream API 為您提供了幾種方法來執(zhí)行此操作。
你已經(jīng)看到了一個(gè),是調(diào)用該流上的 limit()。讓我們重寫前面的示例,并修復(fù)它。
Stream<String> generated = Stream.generate(() -> "+");
List<String> strings =
generated
.limit(10L)
.collect(Collectors.toList());
System.out.println("strings = " + strings);
運(yùn)行此代碼將打印以下內(nèi)容。
strings = [+, +, +, +, +, +, +, +, +, +]
limit() 方法稱為短路方法:它可以停止流元素的消費(fèi)。
從unary operator和種子創(chuàng)建流
如果您需要生成常量的流,使用supplier非常有用。如果你需要一個(gè)具有不同值的無限流,那么你可以使用 iterate() 模式。
此模式適用于種子,種子是第一個(gè)生成的元素。然后,它使用 UnaryOperator 轉(zhuǎn)換前一個(gè)元素來生成流的下一個(gè)元素。
Stream<String> iterated = Stream.iterate("+", s -> s + "+");//根據(jù)前后關(guān)系函數(shù),挨個(gè)生成無限流
iterated.limit(5L).forEach(System.out::println);
您應(yīng)該看到以下結(jié)果。
+
++
+++
++++
+++++
使用此模式時(shí),不要忘記限制元素?cái)?shù)。
從 Java SE 9 開始,此模式具有重載,它將Predicate作為參數(shù)。當(dāng)此Predicate變?yōu)?false 時(shí),iterate() 方法將停止生成元素。前面的代碼可以通過以下方式使用此模式。
Stream<String> iterated = Stream.iterate("+", s -> s.length() <= 5, s -> s + "+");
iterated.forEach(System.out::println);
運(yùn)行此代碼會得到與上一個(gè)代碼相同的結(jié)果。
從一系列數(shù)字創(chuàng)建流
使用以前的模式可以創(chuàng)建一系列數(shù)字。但是,使用專門的數(shù)字流及其 range() 工廠方法會更容易。
range() 接收初始值和范圍的上限(不包含)為參數(shù)。也可以在 rangeClosed() 方法中包含上限。調(diào)用 LongStream.range(0L, 10L) 將簡單地生成一個(gè)流,其中所有l(wèi)ong都在 0 到 9 之間。
這個(gè) range() 方法也可以用來遍歷數(shù)組的元素。這是您可以做到這一點(diǎn)的方法。
String[] letters = {"A", "B", "C", "D"};
List<String> listLetters =
IntStream.range(0, 10)
.mapToObj(index -> letters[index % letters.length])//實(shí)現(xiàn)了數(shù)組的遍歷
.collect(Collectors.toList());
System.out.println("listLetters = " + listLeters);
結(jié)果如下。
listLetters = [A, B, C, D, A, B, C, D, A, B]
基于此模式,您可以做很多事情。請注意,由于 IntStream.range() 創(chuàng)建了一個(gè) IntStream(原始類型流),因此您需要使用 mapToObj() 方法將其轉(zhuǎn)換為對象流。
創(chuàng)建隨機(jī)數(shù)流
Random類用于創(chuàng)建隨機(jī)數(shù)字序列。從 Java SE 8 開始,已向此類添加了幾個(gè)方法來創(chuàng)建不同類型的隨機(jī)數(shù)流int,long,double
您可以創(chuàng)建提供種子參數(shù)的Random實(shí)例。此種子是一個(gè)long。隨機(jī)數(shù)取決于該種子。對于給定的種子,您將始終獲得相同的數(shù)字序列。這在許多情況下可能很方便,包括編寫測試。這種情況下,數(shù)字序列可以預(yù)先知道。
有三種方法可以生成這樣的流,它們都在 Random 類中定義:ints()、longs() 和doubles()。
所有這些方法都有幾個(gè)重載可用,它們接受以下參數(shù):
- 此流將生成的元素?cái)?shù);
- 生成的隨機(jī)數(shù)的上限和下限。
下面是生成 10 個(gè)介于 1 和 5 之間的隨機(jī)整數(shù)的第一種代碼模式。
Random random = new Random(314L);
List<Integer> randomInts =
random.ints(10, 1, 5)
.boxed()//裝箱
.collect(Collectors.toList());
System.out.println("randomInts = " + randomInts);
如果您使用的種子與此示例中使用的種子相同,則控制臺中將具有以下內(nèi)容。
randomInts = [4, 4, 3, 1, 1, 1, 2, 2, 4, 2]
請注意,我們在專用數(shù)字流中使用了 boxed() 方法,它只是將此流map為等效的包裝器類型流。因此,通過此方法將 IntStream 轉(zhuǎn)換為 Stream<Integer>。
這是第二種模式。該流的任何元素都是true,概率為 80%。
Random random = new Random(314L);
List<Boolean> booleans =
random.doubles(1_000, 0d, 1d)
.mapToObj(rand -> rand <= 0.8) // you can tune the probability here
.collect(Collectors.toList());
// Let us count the number of true in this list
long numberOfTrue =
booleans.stream()
.filter(b -> b)//b本身就是boolean
.count();
System.out.println("numberOfTrue = " + numberOfTrue);
如果您使用的種子與我們在本示例中使用的種子相同,您將看到以下結(jié)果。
numberOfTrue = 773
您可以調(diào)整此模式以生成具有所需概率的任何類型的對象。下面是另一個(gè)示例,它生成帶有字母 A、B、C 和 D 的流。每個(gè)字母的概率如下:
- A的50%;
- B的30%;
- C的10%;
- D的10%。
Random random = new Random(314L);
List<String> letters =
random.doubles(1_000, 0d, 1d)
.mapToObj(rand ->
rand < 0.5 ? "A" : // 50% of A
rand < 0.8 ? "B" : // 30% of B
rand < 0.9 ? "C" : // 10% of C
"D") // 10% of D
.collect(Collectors.toList());
Map<String, Long> map =
letters.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));//每個(gè)出現(xiàn)的次數(shù)
map.forEach((letter, number) -> System.out.println(letter + " :: " + number));
使用相同的種子,您將獲得以下結(jié)果。
A :: 470
B :: 303
C :: 117
D :: 110
此時(shí),使用此 groupingBy() 構(gòu)建map可能看起來不明白。不用擔(dān)心,本教程稍后將介紹。
從字符串的字符創(chuàng)建流
String 類在 Java SE 8 中添加了一個(gè) chars() 方法。此方法返回一個(gè) IntStream,該 IntStream 為您提供字符。
每個(gè)字符都作為一個(gè)整數(shù)給出, ASCII 碼。在某些情況下,您可能需要轉(zhuǎn)換為字符串。
您有兩種模式可以執(zhí)行此操作,具體取決于您使用的 JDK 版本。
在 Java SE 10 之前,您可以使用以下代碼。
String sentence = "Hello Duke";
List<String> letters =
sentence.chars()
.mapToObj(codePoint -> (char)codePoint)//int => char
.map(Object::toString)// char =>String
.collect(Collectors.toList());
System.out.println("letters = " + letters);
在 Java SE 11 的 Character 類中添加了一個(gè) toString() 工廠方法,您可以使用它來簡化此代碼。
String sentence = "Hello Duke";
List<String> letters =
sentence.chars()
.mapToObj(Character::toString)//int =>String
.collect(Collectors.toList());
System.out.println("letters = " + letters);
兩個(gè)代碼都打印出以下內(nèi)容。
letters = [H, e, l, l, o, , D, u, k, e]
從文本文件的行創(chuàng)建流
能夠在文本文件上打開流是一種非常強(qiáng)大的模式。
Java I/O API 有一個(gè)模式,能從文本文件中讀取一行:BufferedReader.readLine()。您可以循環(huán)調(diào)用此方法,逐行讀取整個(gè)文本。
使用 Stream API 能為你提供更具可讀性和更易于維護(hù)的代碼。
有幾種模式可以創(chuàng)建這樣的流。
如果需要基于buffered reader重構(gòu)現(xiàn)有代碼,則可以使用在此對象上定義的lines()方法。如果要編寫新代碼,可以使用工廠方法 Files.lines()。最后一種方法將 Path 作為參數(shù),并具有一個(gè)重載方法,添加 CharSet為參數(shù),以防文件不是以 UTF-8 編碼。
您可能知道,文件資源與任何 I/O 資源一樣,當(dāng)不再需要時(shí),應(yīng)將其關(guān)閉。
好消息是Stream接口實(shí)現(xiàn)了AutoCloseable。流本身就是一個(gè)資源,您可以在需要時(shí)關(guān)閉它。上面您看到的所有示例都運(yùn)行在內(nèi)存中,并不需要,但下面情況下肯定是必需的。
下面是計(jì)算日志文件中警告數(shù)量的示例。
Path log = Path.of("/tmp/debug.log"); // adjust to fit your installation
try (Stream<String> lines = Files.lines(log)) {
long warnings =
lines.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Number of warnings = " + warnings);
} catch (IOException e) {
// do something with the exception
}
try-with-resources模式將調(diào)用流的 close() 方法,該方法將正確關(guān)閉已解析的文本文件。
從正則表達(dá)式創(chuàng)建流
這一系列模式的最后一個(gè)示例是添加到 Pattern 類的方法,用于在將正則表達(dá)式應(yīng)用于字符串生成的元素上創(chuàng)建流。
假設(shè)您需要用給定的分隔符拆分字符串。您有兩種模式來執(zhí)行此操作。
- 你可以調(diào)用
String.split()方法; - 或者,您可以使用
Pattern.compile().split()模式。
這兩種模式都為您提供了一個(gè)字符串?dāng)?shù)組,其中包含拆分后的結(jié)果元素。
上面您看到了從此數(shù)組創(chuàng)建流的模式。讓我們編寫此代碼。
String sentence = "For there is good news yet to hear and fine things to be seen";
String[] elements = sentence.split(" ");
Stream<String> stream = Arrays.stream(elements);
Pattern類也有一個(gè)適合你的方法。你可以調(diào)用 Pattern.compile().splitAsStream()。下面是可以使用此方法編寫的代碼。
String sentence = "For there is good news yet to hear and fine things to be seen";
Pattern pattern = Pattern.compile(" ");
Stream<String> stream = pattern.splitAsStream(sentence);//
List<String> words = stream.collect(Collectors.toList());
System.out.println("words = " + words);
運(yùn)行此代碼將生成以下結(jié)果。
words = [For, there, is, good, news, yet, to, hear, and, fine, things, to, be, seen]
您可能想知道這兩種模式中哪一種最好。要回答這個(gè)問題,您需要仔細(xì)查看下。第一種模式首先,創(chuàng)建一個(gè)數(shù)組來存儲拆分的結(jié)果,然后在此數(shù)組上創(chuàng)建一個(gè)流。
在第二種模式中沒有創(chuàng)建數(shù)組,因此開銷更少。
您已經(jīng)看到某些流可能使用短路操作(本教程稍后將詳細(xì)介紹這一點(diǎn))。如果您有這樣的流,拆分整個(gè)字符串并創(chuàng)建生成的數(shù)組可能是一個(gè)重要但無用的開銷。流管道不一定非要使用其所有元素才能生成結(jié)果。
即使您的流需要使用所有元素,將所有這些元素存儲在數(shù)組中仍然是不必要的。
因此,使用 splitAsStream() 模式更好。在內(nèi)存和 CPU 方面更好。
使用builder模式創(chuàng)建流
使用此模式創(chuàng)建流的過程分為兩個(gè)步驟。首先,在builder中添加流將使用的元素。然后,從此builder創(chuàng)建流。使用builder創(chuàng)建流后,您將無法向其添加更多元素,也無法再次使用它來構(gòu)建另一個(gè)流。如果你這樣做,你會得到一個(gè)IllegalStateException。
模式如下。
Stream.Builder<String> builder = Stream.<String>builder();
builder.add("one")
.add("two")
.add("three")
.add("four");
Stream<String> stream = builder.build();//無法再次更改
List<String> list = stream.collect(Collectors.toList());
System.out.println("list = " + list);
運(yùn)行此代碼將打印以下內(nèi)容。
list = [one, two, three, four]
在 HTTP 源上創(chuàng)建流
我們在本教程中介紹的最后一個(gè)模式是關(guān)于分析 HTTP 響應(yīng)的主體。您看到您可以在文本文件的行上創(chuàng)建流,也可以在 HTTP 響應(yīng)的正文上執(zhí)行相同的操作。此模式由添加到 JDK 11 的 HTTP Client API 提供。
這是它的工作原理。我們將在在線提供的文本中使用它:查爾斯狄更斯的《雙城記》,由古騰堡項(xiàng)目在線提供:https://www.gutenberg.org/files/98/98-0.txt
文本文件的開頭提供有關(guān)文本本身的信息。這本書的開頭是“A TALE OF TWO CITIES”。文件的末尾是分發(fā)此文件的許可證。
我們只需要本書的文本,并希望刪除此分布式文件的頁眉和頁腳。
// The URI of the file
URI uri = URI.create("https://www.gutenberg.org/files/98/98-0.txt");
// The code to open create an HTTP request
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).build();
// The sending of the request
HttpResponse<Stream<String>> response = client.send(request, HttpResponse.BodyHandlers.ofLines());
List<String> lines;
try (Stream<String> stream = response.body()) {
lines = stream
.dropWhile(line -> !line.equals("A TALE OF TWO CITIES"))
.takeWhile(line -> !line.equals("*** END OF THE PROJECT GUTENBERG EBOOK A TALE OF TWO CITIES ***"))
.collect(Collectors.toList());
}
System.out.println("# lines = " + lines.size());
運(yùn)行此代碼將打印出以下內(nèi)容。
# lines = 15904
流由您提供的body handler創(chuàng)建,作為 send() 方法的參數(shù)。HTTP Client API 為您提供了多個(gè)body handler。上面是由工廠方法 HttpResponse.BodyHandlers.ofLines() 創(chuàng)建的。這種消費(fèi)響應(yīng)主體的方式非常節(jié)省內(nèi)存。如果仔細(xì)編寫流,響應(yīng)的正文將永遠(yuǎn)不會存儲在內(nèi)存中。
我們這里將所有文本行放在一個(gè)列表中,但是,您不一定需要這樣做。實(shí)際上,大多數(shù)情況下,將此數(shù)據(jù)存儲在內(nèi)存中可能是一個(gè)壞主意。
reduce流
reduce流
到目前為止,您在本教程中了解到,reduce流包括以類似于 SQL 語言中的方式聚合該流的元素。在您運(yùn)行的示例中,您還使用collect(Collectors.toList())模式在列表中收集了您構(gòu)建的流的元素。所有這些操作在Stream API 中稱為末端操作,包括reduce流。
在流上調(diào)用末端操作時(shí),需要記住兩件事。
- 沒有末端操作的流不會處理任何數(shù)據(jù)。如果您在應(yīng)用程序中發(fā)現(xiàn)這樣的流,則很可能是一個(gè)錯(cuò)誤。
- 一個(gè)流同時(shí)只能有一個(gè)中繼或末端操作調(diào)用。您不能重復(fù)使用流;如果你嘗試這樣做,你會得到一個(gè)
IllegalStateException。
使用binary operator來reduce流
在 Stream 接口中定義的 reduce() 方法有三個(gè)重載。它們都接收 BinaryOperator 對象作為參數(shù)。讓我們看看如何使用這個(gè)binary operator。
讓我們舉個(gè)例子。假設(shè)您有一個(gè)整數(shù)列表,您需要計(jì)算這些整數(shù)的總和。您可以使用經(jīng)典的 for 循環(huán)模式編寫以下代碼來計(jì)算此總和。
List<Integer> ints = List.of(3, 6, 2, 1);
int sum = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
sum += ints.get(index);
}
System.out.println("sum = " + sum);
運(yùn)行它會打印出以下結(jié)果。
sum = 12
此代碼的作用如下。
- 將列表中的前兩個(gè)元素相加。
- 然后取下一個(gè)元素并將其求和到您計(jì)算的部分總和。
- 重復(fù)該過程,直到到達(dá)列表末尾。
如果仔細(xì)檢查此代碼,可以使用binary operator對 SUM 運(yùn)算符進(jìn)行建模,以獲得相同的結(jié)果。然后,代碼將變?yōu)橐韵聝?nèi)容。
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;//把操作邏輯提取為lambda
int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
result = sum.apply(result, ints.get(index));
}
System.out.println("sum = " + result);
現(xiàn)在您可以看到此代碼僅依賴于binary operator本身。假設(shè)您需要計(jì)算一個(gè) MAX。您需要做的就是為此提供正確的binary operator。
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> max = (a, b) -> a > b ? a: b;//提供具體函數(shù)即可
int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
result = max.apply(result, ints.get(index));
}
System.out.println("max = " + result);
結(jié)論是,您確實(shí)可以僅提供binary operator來計(jì)算reduce。這就是 reduce() 方法在 Stream API 中的工作方式。
選擇可以并行使用的binary operator
不過,您需要了解兩個(gè)注意事項(xiàng)。讓我們在這里介紹第一個(gè)。
第一個(gè)是可以并行計(jì)算的流。本教程稍后將更詳細(xì)地介紹這一點(diǎn),但現(xiàn)在需要討論它,因?yàn)樗鼘@個(gè)binary operator有影響。數(shù)據(jù)源分為兩部分,每部分單獨(dú)處理。每個(gè)進(jìn)程都與您剛剛看到的進(jìn)程相同,它使用binary operator。然后,在處理每個(gè)部分時(shí),兩個(gè)部分結(jié)果將使用相同的binary operator合并。

并行處理這個(gè)數(shù)據(jù)流非常簡單:只需在給定流上調(diào)用 parallel() 即可。
讓我們來看看事情是如何工作的,為此,您可以編寫以下代碼。您只是在模擬如何并行執(zhí)行計(jì)算。當(dāng)然,這是并行流的過度簡化版本,只是為了解釋事情是如何工作的。
讓我們創(chuàng)建一個(gè) reduce() 方法,該方法接收binary operator并使用它來reduce整數(shù)列表。代碼如下。
int reduce(List<Integer> ints, BinaryOperator<Integer> sum) {
int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
result = sum.apply(result, ints.get(index));
}
return result;
}
下面是使用此方法的主要代碼。
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;//兩個(gè)Integer的具體操作
int result1 = reduce(ints.subList(0, 2), sum);
int result2 = reduce(ints.subList(2, 4), sum);
int result = sum.apply(result1, result2);
System.out.println("sum = " + result);
為了讓過程更明顯,我們將您的數(shù)據(jù)源分為兩部分,并將它們分別reduce為兩個(gè)整數(shù):reduce1和reduce2 。然后,我們使用相同的binary operator合并了這些結(jié)果。這基本上就是并行流的工作方式。
這段代碼非常簡化,它只是為了顯示你的binary operator應(yīng)該具有的一個(gè)非常特殊的屬性。拆分流的方式不應(yīng)影響計(jì)算結(jié)果。以下所有拆分都應(yīng)提供相同的結(jié)果:
3 + (6 + 2 + 1)(3 + 6) + (2 + 1)(3 + 6 + 2) + 1
這表明您的binary operator應(yīng)該具有一個(gè)稱為結(jié)合性 associativity的已知屬性。傳遞給 reduce() 方法的binary operator應(yīng)該是可結(jié)合的。
Stream API 的 reduce() 重載版本, JavaDoc API 文檔指出,您作為參數(shù)提供的binary operator必須是可結(jié)合的。
如果不是這樣,會發(fā)生什么?嗯,這正是問題所在:編譯器和 Java 運(yùn)行時(shí)都不會檢測到它。因此,您的數(shù)據(jù)將被處理,沒有明顯的錯(cuò)誤。你可能有正確的結(jié)果,也可能沒有;這取決于內(nèi)部處理數(shù)據(jù)的方式。事實(shí)上,如果你多次運(yùn)行代碼,你最終可能會得到不同的結(jié)果。這是您需要注意的非常重要的一點(diǎn)。
如何測試binary operator是否可結(jié)合?在某些情況下,這可能非常簡單:SUM,MIN,MAX是眾所周知的可結(jié)合運(yùn)算符。在其他一些情況下,這可能要困難得多。檢查的一種方法,可以是在隨機(jī)數(shù)據(jù)上運(yùn)行binary operator,并驗(yàn)證是否始終獲得相同的結(jié)果。
管理具有幺元的binary operator
第二個(gè)是binary operator這種結(jié)合性產(chǎn)生的結(jié)果。
此結(jié)合性屬性是由以下事實(shí)保證的:數(shù)據(jù)的拆分方式不應(yīng)影響計(jì)算結(jié)果。如果將集合 A 拆分為兩個(gè)子集 B 和 C,則reduce A 應(yīng)該得到與reduce (B 的reduce和 C 的reduce)相同的結(jié)果。
可以將前面的屬性寫入更通用的表達(dá)式:
A = B ? C ? Red(A) = Red(Red(B), Red(C))
事實(shí)證明,這導(dǎo)致了另一個(gè)問題。假設(shè)事情出了意外,B實(shí)際上是空的。這種情況下,C = A。前面的表達(dá)式變?yōu)橐韵聝?nèi)容:
Red(A) = Red(Red(?), Red(A)) //必須成立才行
當(dāng)且僅當(dāng)空集 (?) 的reduce是reduce操作的幺元identity element時(shí),才是正確的。
這是數(shù)據(jù)處理中的一種屬性:空集的reduce是reduce操作的幺元。
在數(shù)據(jù)處理中這確實(shí)是一個(gè)問題,尤其是在并行處理中,一些非常經(jīng)典的binary operator并沒有幺元,比如 MIN 和 MAX??占淖钚≡貨]有定義,因?yàn)?MIN 操作沒有幺元。
此問題必須在Stream API 中解決,因?yàn)槟赡鼙仨毺幚砜樟鳌D吹搅藙?chuàng)建空流的模式,很容易看出 filter() 可以filter掉所有數(shù)據(jù),從而返回空流。
Stream API 是這樣處理的。幺元未知(不存在或未提供)的reduce將返回 Optional 類的實(shí)例。我們將在本教程后面更詳細(xì)地介紹此類。此時(shí)您需要知道的是,此 Optional 類是一個(gè)可以為空的包裝類。每次對沒有已知幺元的流調(diào)用末端操作時(shí),Stream API 都會將結(jié)果包裝在該對象中。如果處理的流為空,則此Optional對象也將為空,下一步如何處理將由您和您的應(yīng)用程序決定。
探索Stream API 的reduce方法
正如我們前面提到的,Stream API 有三個(gè)重載的 reduce() 方法,我們現(xiàn)在可以詳細(xì)介紹這些重載。
使用幺元進(jìn)行reduce
第一個(gè)接收幺元和 BinaryOperator 的實(shí)例。由于您提供的第一個(gè)參數(shù)是binary operator的已知幺元,因此具體實(shí)現(xiàn)可能會使用它來簡化計(jì)算。從這個(gè)幺元開始,啟動進(jìn)程,而不是選兩個(gè)元素。使用的算法具有以下形式。
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;
int identity = 0;
int result = identity;//人為設(shè)定幺元,初始值
for (int i: ints) {
result = sum.apply(result, i);
}
System.out.println("sum = " + result);
你可以注意到,即使你需要處理的列表是空的,這種編寫方式也能很好地工作。這種情況下,它將返回幺元,這是您需要的。
API 不會檢查您提供的元素確實(shí)是binary operator的幺元這一事實(shí)。提供不對的元素將返回錯(cuò)誤的結(jié)果。
您可以在以下示例中看到這一點(diǎn)。
Stream<Integer> ints = Stream.of(0, 0, 0, 0);
int sum = ints.reduce(10, (a, b) -> a + b);//初始值為10
System.out.println("sum = " + sum);
您希望此代碼在控制臺上打印值 0。因?yàn)?reduce() 方法調(diào)用的第一個(gè)參數(shù)不是binary operator的幺元,所以結(jié)果實(shí)際上是錯(cuò)誤的。運(yùn)行此代碼將在主機(jī)上打印以下內(nèi)容。
sum = 10
這是您應(yīng)該使用的正確代碼。
Stream<Integer> ints = Stream.of(0, 0, 0, 0);
int sum = ints.reduce(0, (a, b) -> a + b);//初始值為0
System.out.println("sum = " + sum);
此示例說明在編譯或運(yùn)行代碼時(shí)傳遞錯(cuò)誤的幺元不會觸發(fā)任何錯(cuò)誤或異常。具體取決于您。
此屬性的測試方式可以跟測試結(jié)合性相同。將候選幺元與盡可能多的值組合在一起。如果您找到一個(gè)因組合而改變的值,那么您的幺元就不是合適的候選。反之并不成立,如果您找不到任何錯(cuò)誤的組合,并不一定意味著您的候選就是正確的。
不使用幺元進(jìn)行reduce
reduce() 方法的第二個(gè)重載接收沒有幺元的 BinaryOperator 實(shí)例作為參數(shù)。正如預(yù)期的那樣,它返回一個(gè) Optional 對象,包裝reduce的結(jié)果。使用Optional做的最簡單的事情,就是打開并查看其中是否有任何東西。
讓我們舉一個(gè)沒有幺元的reduce示例。
Stream<Integer> ints = Stream.of(2, 8, 1, 5, 3);
Optional<Integer> optional = ints.reduce((i1, i2) -> i1 > i2 ? i1: i2);//大于空集沒有意義,可能為null
if (optional.isPresent()) {
System.out.println("result = " + optional.orElseThrow());
} else {
System.out.println("No result could be computed");
}
運(yùn)行此代碼將產(chǎn)生以下結(jié)果。
result = 8
請注意,此代碼使用 orElseThrow() 方法打開可選代碼,該方法現(xiàn)在是執(zhí)行此操作的首選方法。此模式已在 Java SE 10 中添加,以取代最初在 Java SE 8 中引入的更傳統(tǒng)的 get() 方法。
get()方法的問題在于,如果Optional為空,可能會拋出一個(gè)NoSuchElementException。此方法的命名 orElseThrow() 比 get() 更直觀,它提醒您,打開空的Optional您將收到異常。
使用Optional可以完成更多操作,您將在本教程后面了解。
在一種方法中組合map和reduce
第三個(gè)稍微復(fù)雜一些。它使用多個(gè)參數(shù)組合combine了內(nèi)部mapping和reduce。
讓我們檢查一下此方法的簽名。
<U> U reduce(U identity,//幺元
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);//兩個(gè)U類型的具體操作
類型U在本地定義并由binary operator使用。binary operator的工作方式與 reduce() 剛才那個(gè)重載相同,只是它不應(yīng)用于流的元素,而僅應(yīng)用于它們mapping后的版本。
這種mapping和reduce本身實(shí)際上組合為一個(gè)操作:累加器accumulator。請記住,在本部分的開頭,您看到reduce是逐步進(jìn)行的,并且一次消費(fèi)一個(gè)元素。在每一步,reduce操作的第一個(gè)參數(shù)是到目前為止消費(fèi)的所有元素的reduce部分。
幺元是combiner的幺元。的確是這樣。
假設(shè)您有一個(gè) String 實(shí)例流,您需要對所有字符串的長度求和。
combiner組合了兩個(gè)部分:到目前處理的字符串長度的部分總和,兩個(gè)整數(shù)。
accumulator從流中獲取一個(gè)元素,將其map為一個(gè)整數(shù)(該字符串的長度),并將其添加到到目前為止計(jì)算的總和中。
以下是該算法的工作原理。

相應(yīng)的代碼如下。
Stream<String> strings = Stream.of("one", "two", "three", "four");
BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;//兩個(gè)Integer部分總和,具體操作
//累加mapping操作:部分總和Integer,跟新元素String作運(yùn)算,返回新的部分總和Integer
BiFunction<Integer, String, Integer> accumulator =
(partialReduction, element) -> partialReduction + element.length();
int result = strings.reduce(0, accumulator, combiner);//combiner的初始值為0
System.out.println("sum = " + result);
運(yùn)行此代碼將生成以下結(jié)果。
sum = 15
在上面的示例中,mapping過程實(shí)際為以下函數(shù)。
Function<String, Integer> mapper = String::length;
因此,您可以將accumulator重寫為以下模式。這種寫法清楚地顯示了mapping的組合過程。
Function<String, Integer> mapper = String::length;//mapping
BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;
BiFunction<Integer, String, Integer> accumulator =
(partialReduction, element) -> partialReduction + mapper.apply(element);
在流上添加末端操作
避免使用reduce方法
如果流不以末端操作結(jié)束,則不會處理任何數(shù)據(jù)。我們已經(jīng)介紹了末端操作 reduce(),您在其他示例中看到了幾個(gè)末端操作?,F(xiàn)在讓我們介紹其他幾個(gè)。
使用 reduce() 方法并不是reduce流的最簡單方法。您需要確保您提供的binary operator是可結(jié)合的,然后您需要知道它是否具有幺元。您需要檢查許多點(diǎn),以確保您的代碼正確并產(chǎn)生您期望的結(jié)果。如果你可以避免使用 reduce() 方法,那么你絕對應(yīng)該這樣做,因?yàn)樗苋菀壮鲥e(cuò)。
幸運(yùn)的是,Stream API 為您提供了許多其他reduce流的方法:我們在介紹專門的數(shù)字流時(shí)介紹的 sum()、min() 和 max() 是您可以使用的便捷方法。事實(shí)上,你只能吧 reduce() 方法作為最后的手段,只有當(dāng)你沒有其他解決方案時(shí)。
計(jì)算元素?cái)?shù)量
count() 方法存在于所有流接口中,包括專用流和對象流。它用long返回該流處理的元素?cái)?shù)。這個(gè)數(shù)字可能很大,實(shí)際上大于 Integer.MAX_VALUE。
您可能想知道為什么需要如此多的數(shù)字。實(shí)際上,您可以從許多源創(chuàng)建流,包括可以生成大量元素的源,大于 Integer.MAX_VALUE。即使不是這種情況,也很容易創(chuàng)建一個(gè)中繼操作,將流處理的元素?cái)?shù)量成倍增加。我們在本教程前面介紹的 flatMap() 方法可以做到這一點(diǎn)。有很多方法可以讓你最終超過 Integer.MAX_VALUE 。這就是 Stream API 支持它的原因。
下面是 count() 方法的一個(gè)示例。
Collection<String> strings =
List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
long count =
strings.stream()
.filter(s -> s.length() == 3)
.count();
System.out.println("count = " + count);
運(yùn)行此代碼將生成以下結(jié)果。
count = 4
逐個(gè)消費(fèi)元素
Stream API 的 forEach() 方法允許您將流的每個(gè)元素傳遞給Consumer接口的實(shí)例。此方法對于打印流處理的元素非常方便。這就是以下代碼的作用。
Stream<String> strings = Stream.of("one", "two", "three", "four");
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.forEach(System.out::println);
運(yùn)行此代碼將打印以下內(nèi)容。
ONE
TWO
這種方法非常簡單,但您可能會用錯(cuò)。
請記住,您編寫的 lambda 表達(dá)式應(yīng)避免改變其外部作用域。有時(shí),在狀態(tài)外發(fā)生突變稱為傳導(dǎo)副作用。剛才的Consumer很特殊,因?yàn)闆]有什么特別的副作用。實(shí)際上也有,調(diào)用 System.out.println() 會對應(yīng)用程序的控制臺產(chǎn)生副作用。
讓我們考慮以下示例。
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = new ArrayList<>();
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.forEach(result::add);
System.out.println("result = " + result);
運(yùn)行前面的代碼會打印出以下內(nèi)容。
result = [ONE, TWO]
因此,您可能會想使用此代碼,因?yàn)樗芎唵危夷堋罢9ぷ鳌薄:冒桑@段代碼正在做一些錯(cuò)誤的事情。讓我們來看看它們。
從流中調(diào)用result::add,將該流處理的所有元素添加到外部result列表中。此Consumer正在對流本身范圍之外的變量產(chǎn)生副作用。
訪問此類變量會使您的 lambda 表達(dá)式成為捕獲式 lambda 表達(dá)式。創(chuàng)建這樣的 lambda 表達(dá)式雖然完全合法,但會降低性能。如果性能是應(yīng)用程序中的重要問題,則應(yīng)避免編寫捕獲式 lambda。
此外,這種方式也會阻止此流的并行。實(shí)際上,如果您嘗試使此流并行,您將有多個(gè)線程并行訪問您的result列表。而 ArrayList 并不是并發(fā)安全的類。
有兩種變通模式收集到列表。下面的示例演示使用集合對象。
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
這段代碼同樣創(chuàng)建 ArrayList 的實(shí)例,并將流處理的元素添加到其中。不會產(chǎn)生任何副作用,因此不會對性能造成影響。
并行性和并發(fā)性由Collector API 本身處理,因此您可以安全地使此流并行。
從Java SE 16開始,您有第二種更簡單的模式,使用collector對象。
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.toList();
此模式生成 List 的特殊不可變實(shí)例。如果你需要一個(gè)可變列表,你應(yīng)該使用上一種。另外,它還比在 ArrayList 中收集流的性能更好。這一點(diǎn)將在下一段介紹。
收集到集合或數(shù)組中
Stream API 提供了多種將流元素收集到集合中的方法。在上一節(jié)中,您初步了解了其中兩種。讓我們看看其他的。
在選擇所需的模式之前,您需要問自己幾個(gè)問題。
- 是否需要構(gòu)建不可變列表?
- 你對
ArrayList的實(shí)例感到滿意嗎?或者你更喜歡LinkedList? - 您是否確切地知道您的流將處理多少個(gè)元素?
- 您是否需要在精確的、可能是第三方或自制的
List中收集您的元素?
Stream API 可以處理所有這些情況。
在ArrayList中收集
您已經(jīng)在前面的示例中使用了此模式。它是您可以使用的最簡單的方法,并返回 ArrayList 實(shí)例中的元素。
下面是這種模式的實(shí)際示例。
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
此模式創(chuàng)建 ArrayList 的簡單實(shí)例,并在其中累積流的元素。如果有太多元素, ArrayList 的內(nèi)部數(shù)組無法存儲它們,則當(dāng)前數(shù)組將被復(fù)制到一個(gè)更大的數(shù)組中,并由GC回收。
如果你想避免這種情況,并且知道你的流將產(chǎn)生的元素?cái)?shù)量,那么你可以使用 Collectors.toCollection() ,它以supplier作為參數(shù)來創(chuàng)建集合,你將在其中收集處理的元素。以下代碼使用此模式創(chuàng)建初始容量為 10,000 的 ArrayList 實(shí)例。
Stream<String> strings = ...;
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toCollection(() -> new ArrayList<>(10_000)));
在不可變List中收集
在某些情況下,您需要在不可變列表中累積元素。這聽起來可能自相矛盾,因?yàn)槭占馕吨鴮⒃靥砑拥奖仨毧勺兊娜萜髦?。?shí)際上,這就是Collector API 的工作方式,本教程后面將詳細(xì)介紹。在此累加操作結(jié)束時(shí),Collector API 可以繼續(xù)執(zhí)行最后一個(gè)可選操作,本例中,在返回之前密封這個(gè)列表。
為此,您只需使用以下模式。
Stream<String> strings = ...;
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toUnmodifiableList()));
在此示例中,result是一個(gè)不可變列表。
從 Java SE 16 開始,有一種更好的方法可以在不可變列表中收集數(shù)據(jù),這在某些情況下可能更有效。模式如下。
Stream<String> strings = ...;
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.toList();
如何提高效率的?第一種模式是建立在使用collector的基礎(chǔ)上的,首先在普通 ArrayList 中收集元素,然后將其密封,使其在處理完成后不可變。您的代碼看到的只是從此 ArrayList 構(gòu)建的不可變列表。
如您所知,ArrayList 的實(shí)例是在具有固定大小的內(nèi)部數(shù)組上構(gòu)建的。此列表可能已滿。這種情況下,ArrayList 實(shí)現(xiàn)會檢測到并將其復(fù)制到更大的數(shù)組中。此機(jī)制對使用者是透明的,但會帶來開銷:復(fù)制此數(shù)組需要一些時(shí)間。
在某些情況下,在消費(fèi)所有流之前,Stream API 可以跟蹤要處理的元素?cái)?shù)量。這種情況下,創(chuàng)建大小合適的內(nèi)部數(shù)組更有效,因?yàn)樗苊饬藢⑿?shù)組到較大數(shù)組的開銷。
Stream.toList() 方法已添加到 Java SE 16 中。如果您需要的是不可變的列表,那么您應(yīng)該使用此模式。
在自制List中收集
如果您需要在自己的列表或 JDK 之外的第三方List中收集數(shù)據(jù),則可以使用 Collectors.toCollection() 模式。用于調(diào)整 ArrayList 初始大小的supplier也可用于構(gòu)建 Collection 的任何實(shí)現(xiàn),包括不屬于 JDK 的實(shí)現(xiàn)。您所需要的只是一個(gè)supplier。在以下示例中,我們提供了一個(gè)supplier來創(chuàng)建 LinkedList 的實(shí)例。
Stream<String> strings = ...;
List<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toCollection(LinkedList::new));
在Set中收集
由于 Set 接口是 Collection 接口的擴(kuò)展,因此可以使用 Collectors.toCollection(HashSet::new) 在 Set 實(shí)例中收集數(shù)據(jù)。這很好,但 Collector API 仍然為您提供了一個(gè)更簡潔的模式:Collectors.toSet()。
Stream<String> strings = ...;
Set<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toSet());
您可能想知道這兩種模式之間是否有任何區(qū)別。答案是肯定的,存在細(xì)微的區(qū)別,您將在本教程后面看到。
如果你需要的是一個(gè)不可變的集合,Collector API 還有另一種模式:Collectors.toUnmodifiableSet()。
Stream<String> strings = ...;
Set<String> result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toUnmodifiableSet());
在數(shù)組中收集
Stream API 也有自己的一組 toArray() 方法重載。其中有兩個(gè)。
第一個(gè)是普通的 toArray() 方法,它返回Object[] .使用此模式會丟失元素的確切類型。
第二個(gè)參數(shù)接收 IntFunction<A[]> 類型的參數(shù),按照size返回一個(gè)數(shù)組。乍一看可能很嚇人,但編寫此函數(shù)的實(shí)現(xiàn)實(shí)際上非常容易。如果需要構(gòu)建一個(gè)字符串?dāng)?shù)組,則此函數(shù)的實(shí)現(xiàn)為 String[]::new。
Stream<String> strings = ...;
String[] result =
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.toArray(String[]::new);
System.out.println("result = " + Arrays.toString(result));
運(yùn)行此代碼將生成以下結(jié)果。
result = [ONE, TWO]
提取流的最大值和最小值
Stream API 為此提供了幾種方法,具體取決于您當(dāng)前正在使用的流。
我們已經(jīng)介紹了來自專用數(shù)字流的 max() 和 min() 方法:IntStream、LongStream 和 DoubleStream。您知道這些操作沒有幺元,因此所有都將返回Optional。
順便說一下,同樣來自數(shù)字流的 average() 方法也返回一個(gè)Optional對象,因?yàn)?average 操作也沒有幺元。
Stream 接口也有兩個(gè)方法 max() 和 min(),它們也返回一個(gè)Optional對象。與數(shù)字流的區(qū)別在于,Stream的元素實(shí)際上可以是任何類型的。為了能夠計(jì)算最大值或最小值,實(shí)現(xiàn)需要比較這些對象。這就是您需要為這些方法提供comparator的原因。
這是 max() 方法的實(shí)際應(yīng)用。
Stream<String> strings = Stream.of("one", "two", "three", "four");
String longest =
strings.max(Comparator.comparing(String::length))//對象Stream
.orElseThrow();
System.out.println("longest = " + longest);
它將打印以下內(nèi)容。
longest = three
請記住,嘗試打開空的Optional對象會拋出 NoSuchElementException,這是您不希望在應(yīng)用程序中看到的內(nèi)容。僅當(dāng)您的流沒有任何要處理的數(shù)據(jù)時(shí),才會這樣。在這個(gè)簡單的示例中,你有一個(gè)流,它處理多個(gè)字符串,沒有filter操作。此流不會為空,因此您可以安全地打開。
在流中查找元素
Stream API 為您提供了兩個(gè)末端操作來查找元素:findFirst() 和 findAny()。這兩個(gè)方法不接受任何參數(shù),并返回流的單個(gè)元素。為了正確處理空流的情況,此元素包裝在Optional對象中。如果流為空,則此Optional也為空。
了解返回哪個(gè)元素需要您了解流可能是順序的。順序流只是一種流,其中元素的順序很重要,并由Stream API 保存。默認(rèn)情況下,在任何順序源(例如 List 接口的實(shí)現(xiàn))上創(chuàng)建的流本身都是順序的。
在這樣的流上,稱呼第一個(gè)、第二個(gè)或第三個(gè)元素是有意義的。找到這樣一個(gè)流的第一個(gè)元素也是完全有意義的。
如果您的流無序,或者如果順序在流處理中丟失了,則查找第一個(gè)元素是無法定義的,并且調(diào)用 findFirst() 實(shí)際上會返回流的任何元素。您將在本教程后面看到有關(guān)順序流的更多詳細(xì)信息。
請注意,調(diào)用 findFirst() 會在流實(shí)現(xiàn)中觸發(fā)一些檢查,以確保在對該流進(jìn)行排序時(shí)獲得該流的第一個(gè)元素。如果您的流是并行流,這可能代價(jià)很高。在許多情況下,獲取的是不是第一個(gè)元素并無所謂,比如流僅處理單個(gè)元素的情況。在所有這些情況下,您應(yīng)該使用 findAny() 而不是 findFirst()。
讓我們看看 findFirst() 的實(shí)際效果。
Collection<String> strings =
List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
String first =
strings.stream()
// .unordered()
// .parallel()
.filter(s -> s.length() == 3)
.findFirst()
.orElseThrow();
System.out.println("first = " + first);
此流是在 List 的實(shí)例上創(chuàng)建的,這使它成為順序流。請注意,在第一個(gè)版本中注釋了 unordered() 和 parallel() 兩行。
多次運(yùn)行此代碼將始終得到相同的結(jié)果。
first = one
unordered() 中繼方法調(diào)用使順序流成為無序流。這種情況下,它沒有任何區(qū)別,因?yàn)槟牧魇前错樞蛱幚淼?。您的?shù)據(jù)是從始終以相同順序遍歷其元素的列表中提取的。出于同樣的原因,將 findFirst() 方法調(diào)用替換為 findAny() 方法調(diào)用也沒有任何區(qū)別。
可以對此代碼進(jìn)行的第一個(gè)修改是取消注釋 parallel() 方法調(diào)用?,F(xiàn)在,您有一個(gè)并行處理的順序流。多次運(yùn)行此代碼將始終得到相同的結(jié)果:one。這是因?yàn)槟牧魇?em>順序的,因此無論您的流是如何處理的,第一個(gè)元素都是確定的。
要使此流無序,您可以取消注釋 unordered() 方法調(diào)用,或者將(List.of)替換為 Set.of()。在這兩種情況下,使用 findFirst() 終止流將從該并行流返回一個(gè)隨機(jī)元素。并行流的處理方式使其如此。
您可以在此代碼中進(jìn)行的第二個(gè)修改是將 List.of() 替換為 Set.of()。現(xiàn)在不再是順序的。此外,Set.of() 返回的實(shí)現(xiàn),使得集合元素的遍歷以隨機(jī)順序發(fā)生。多次運(yùn)行此代碼會顯示 findFirst() 和 findAny() 都返回一個(gè)隨機(jī)字符串,即使 unordered() 和 parallel() 都注釋掉。查找無序源的第一個(gè)元素?zé)o法定義,結(jié)果是隨機(jī)的。
從這些示例中,您可以推斷出在并行流的實(shí)現(xiàn)中采取了一些預(yù)防措施來跟蹤哪個(gè)元素是第一個(gè)。這造成了開銷,因此,只有在確實(shí)需要時(shí)才應(yīng)調(diào)用 findFirst()。
檢查流的元素是否與Predicate匹配
在某些情況下,在流中查找元素或未能在流中找到元素可能是您真正需要的。您查找的元素不一定與您的應(yīng)用程序有關(guān);但是否存在非常重要。
以下代碼將用于檢查給定元素是否存在。
boolean exists =
strings.stream()
.filter(s -> s.length() == 3)
.findFirst()
.isPresent();
實(shí)際上,此代碼檢查返回的Optional是否為空。
上面的模式工作正常,但Stream API 提供了一種更有效的方法。實(shí)際上,構(gòu)建此Optional對象是一種開銷,如果您使用以下三種方法之一,則無需支付該開銷。這三種方法將Predicate作為參數(shù)。
anyMatch(Predicate):如果找到與給定Predicate匹配的一個(gè)元素,則返回trueallMatch(Predicate):如果流的所有元素都與Predicate匹配,則返回truenoneMatch(Predicate):相反
讓我們看看這些方法的實(shí)際應(yīng)用。
Collection<String> strings =
List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
boolean noBlank =
strings.stream()
.allMatch(Predicate.not(String::isBlank));
boolean oneGT3 =
strings.stream()
.anyMatch(s -> s.length() == 3);
boolean allLT10 =
strings.stream()
.noneMatch(s -> s.length() > 10);
System.out.println("noBlank = " + noBlank);
System.out.println("oneGT3 = " + oneGT3);
System.out.println("allLT10 = " + allLT10);
運(yùn)行此代碼將生成以下結(jié)果。
noBlank = true
oneGT3 = true
allLT10 = true
短路流的處理
您可能已經(jīng)注意到我們在此處介紹的不同末端操作之間的重要差異。
其中一些需要處理流消費(fèi)的所有數(shù)據(jù)。COUNT、MAX、MIN、AVERAGE操作以及forEach()、toList()或toArray()方法調(diào)用就是這種情況。
我們介紹的最后一個(gè)末端操作并非如此。一旦找到元素,findFirst() 或 findAny() 方法就會停止處理您的數(shù)據(jù),無論還有多少元素需要處理。anyMatch()、allMatch() 和 noneMatch() 也是如此:它們可能會中斷流的處理并得到結(jié)果,而不必消費(fèi)源所有元素。
這些方法在Stream API 中稱為短路方法,因?yàn)樗鼈兛梢园肼飞山Y(jié)果,而無需處理流的所有元素。
在某些情況下,這些最后的方法仍然可能處理所有元素:
findFirst()和findAny()返回了空的Optional,只能在所有元素都已處理后。anyMatch()返回了false。- allMatch() 和
noneMatch()返回 了true。
查找流的特征
流的特征
Stream API 依賴于一個(gè)特殊的對象,即 Spliterator 接口的實(shí)例。此接口的名稱來源于這樣一個(gè)事實(shí),即Stream API 中spliterator的角色類似于iterator在集合 API 中的角色。此外,由于Stream API 支持并行處理,因此spliterator對象還控制流在處理并行化時(shí),不同 CPU 之間如何拆分其元素。名稱是split和iterator的組合。
詳細(xì)介紹此spliterator對象超出了本教程的范圍。您需要知道的是,此spliterator對象具有流的特征。這些特征不是您經(jīng)常使用到的,但了解它們是什么將幫助您在某些情況下編寫更好、更高效的管道。
流的特征如下。
| 特征 | 評論 |
|---|---|
| ORDERED | 順序的,處理流元素的順序很重要。 |
| DISTINCT | 去重的,該流處理的元素中沒有重復(fù)出現(xiàn)。 |
| NONNULL | 該流中沒有空元素。 |
| SORTED | 排序的,對該流的元素已經(jīng)進(jìn)行排序。 |
| SIZED | 有數(shù)量的,此流處理的元素?cái)?shù)是已知的。 |
| SUBSIZED | 拆分此流會產(chǎn)生兩個(gè) SIZED 流。 |
有兩個(gè)特征,不可變 IMMUTABLE和并發(fā)的 CONCURRENT,本教程未介紹。
每個(gè)流在創(chuàng)建時(shí)都設(shè)置或取消設(shè)置了所有這些特征。
請記住,可以通過兩種方式創(chuàng)建流。
- 您可以從數(shù)據(jù)源創(chuàng)建流,我們介紹了幾種不同的模式。
- 每次對現(xiàn)有流調(diào)用中繼操作時(shí),都會創(chuàng)建一個(gè)新流。
給定流的特征取決于創(chuàng)建它的源,或者創(chuàng)建它的流的特征,以及創(chuàng)建的操作。如果您的流是使用源創(chuàng)建的,則其特征取決于該源,如果您使用另一個(gè)流創(chuàng)建它,則它們將取決于該其他流以及您正在使用的操作類型。
讓我們更詳細(xì)地介紹每個(gè)特征。
ORDERED流
順序流是使用順序數(shù)據(jù)源創(chuàng)建的。可能想到的第一個(gè)示例是 List 接口的任何實(shí)例。還有其他的:Files.lines(path) 和 Pattern.splitAsStream(string) 也生成 ORDERED 流。
跟蹤流元素的順序可能會導(dǎo)致并行流的開銷。如果不需要此特性,則可以通過在現(xiàn)有流上調(diào)用 unordered() 中繼方法來刪除它。這將返回沒有此特征的新流。你為什么要這樣做?在某些情況下,保持流 ORDERED 可能會很昂貴,例如,當(dāng)您使用并行流時(shí)。
SORTED流
SORTED的流是已排序的流??梢詮囊雅判虻脑矗ㄈ?TreeSet 實(shí)例)或通過調(diào)用 sorted() 方法創(chuàng)建此流。知道流已被排序可能會被流的某些實(shí)現(xiàn)拿來用,以避免再次進(jìn)行排序。但排序后的順序可能會變,因?yàn)?SORTED 流可能會使用與第一次不同的comparator再次排序。
有一些中繼操作可以清除 SORTED 特征。在下面的代碼中,您可以看到strings,filteredStream兩者都是 SORTED 流,而lengths不是。
Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
Stream<String> strings = stringCollection.stream().sorted();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);
mapping或flatmapping SORTED 流會從生成的流中刪除此特征。
DISTINCT流
DISTINCT 流是它正在處理的元素之間沒有重復(fù)項(xiàng)的流。例如,當(dāng)從 HashSet 構(gòu)建流時(shí),或者從對 distinct() 中繼方法調(diào)用的調(diào)用中構(gòu)建流時(shí),可以獲得這樣的特征。
DISTINCT 特征在filtering流時(shí)保留,但在mapping或flatmapping流時(shí)丟失。
讓我們檢查以下示例。
Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
Stream<String> strings = stringCollection.stream().distinct();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);
stringCollection.stream()不是 DISTINCT 的,因?yàn)樗菑?List的實(shí)例構(gòu)建的。strings是 DISTINCT 的,因?yàn)榇肆魇峭ㄟ^調(diào)用distinct()中繼方法創(chuàng)建的。filteredStrings仍然是 DISTINCT:從流中刪除元素不會創(chuàng)造重復(fù)項(xiàng)。length已被map,因此 DISTINCT 特征丟失。
NONNULL 流
非空流是不包含null值的流。集合框架中的一些結(jié)構(gòu)不接受空值,包括 ArrayDeque 和并發(fā)結(jié)構(gòu),如 ArrayBlockingQueue、ConcurrentSkipListSet 和調(diào)用 ConcurrentHashMap.newKeySet() 返回的并發(fā)Set。使用 Files.lines(path) 和 Pattern.splitAsStream(line) 創(chuàng)建的流也是非空流。
至于前面的特征,一些中繼操作可以產(chǎn)生具有不同特征的流。
- filtering或排序非空流將返回非空流。
- 在 NONNULL 流上調(diào)用
distinct()也會返回一個(gè) NONNULL 流。 - mapping或flatmapping NONNULL 流將返回沒有此特征的流。
SIZED和SUBSIZED流
SIZED流
當(dāng)您想要使用并行流時(shí),最后一個(gè)特征非常重要。本教程稍后將更詳細(xì)地介紹并行流。
SIZED 流是知道它將處理多少個(gè)元素的流。從 Collection 的任何實(shí)例創(chuàng)建的流都是這樣的流,因?yàn)?Collection 接口具有 size() 方法,因此獲取此數(shù)字很容易。
另一方面,在某些情況下,您知道流的元素是有限數(shù)的,但除非您處理流本身,否則您無法知道此數(shù)量。
對于使用 Files.lines(path) 模式創(chuàng)建的流,情況就是如此。您可以獲取文本文件的大?。ㄒ宰止?jié)為單位),但此信息不會告訴您此文本文件有多少行。您需要分析文件以獲取此信息。
Pattern.splitAsStream(line) 模式也是。知道您正在分析的字符串中的字符數(shù)并不能給出任何關(guān)于此模式將產(chǎn)生多少元素的提示。
SUBSIZED流
SUBSIZED 特征,與并行流的拆分方式有關(guān)。簡單說,并行化機(jī)制將流分成兩部分,并在 CPU 正在執(zhí)行的不同可用內(nèi)核之間分配計(jì)算。此拆分由流使用的 Spliterator 實(shí)例實(shí)現(xiàn)。具體實(shí)現(xiàn)取決于您使用的數(shù)據(jù)源。
假設(shè)您需要在 ArrayList 上打開一個(gè)流。此列表的所有數(shù)據(jù)都保存在 ArrayList 實(shí)例的內(nèi)部數(shù)組中。也許您還記得 ArrayList 對象上的內(nèi)部數(shù)組是一個(gè)緊湊數(shù)組,每當(dāng)數(shù)組中刪除元素時(shí),所有后續(xù)元素都會向左移動一個(gè)單元格,不會留下任何空位。
這使得拆分 ArrayList 變得簡單明了。要拆分 ArrayList 的實(shí)例,您可以將此內(nèi)部數(shù)組拆分為兩部分,兩部分中的元素?cái)?shù)量相同。這使得在 ArrayList 實(shí)例上創(chuàng)建的流具有 SUBSIZED特性:您甚至可以設(shè)定拆分后每個(gè)部分中將保留多少個(gè)元素。
假設(shè)現(xiàn)在您需要在 HashSet 實(shí)例上打開一個(gè)流。HashSet 將其元素存儲在數(shù)組中,但此數(shù)組的使用方式與 ArrayList 使用的數(shù)組不同。實(shí)際上,多個(gè)元素可以存儲在此數(shù)組的一個(gè)單元格中。拆分這個(gè)數(shù)組沒有問題,但是如果不計(jì)算一下,就無法提前知道每個(gè)部分中將保留多少個(gè)元素。即使你把這個(gè)數(shù)組從中間分開,也無法保證兩半的元素?cái)?shù)量就是相同。這就是為什么在 HashSet 實(shí)例上創(chuàng)建的流是 SIZED而不是 SUBSIZED 。
map流可能會更改返回流的 SIZED 和 SUBSIZED 特征。
- mapping和排序流會保留 SIZED 和 SUBSIZED特征。
- flatmapping、filtering和調(diào)用
distinct()會擦除這些特征。
posted on 2023-07-02 17:20 研發(fā)軟件的郭 閱讀(334) 評論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號