Java 8 Stream API教學
1.概述
在這個深入的教程中,我們將介紹從創建到並行執行的Java 8 Streams的實際用法。
為了理解該材料,讀者需要具有Java 8(lambda表達式, Optional,
方法引用)和Stream API的基礎知識。如果您不熟悉這些主題,請閱讀我們以前的文章-Java 8中的新功能和Java 8 Streams簡介。
2.流創建
有很多方法可以創建不同來源的流實例。創建實例後,實例將不會修改其源,因此允許從單個源創建多個實例。
2.1。空流
在創建空流的情況下,應使用**empty()
**方法:
Stream<String> streamEmpty = Stream.empty();
通常在創建時使用empty()
方法來避免不包含元素的流返回null
的情況:
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
2.2。 Collection
流
流也可以創建任何類型的Collection
( Collection, List, Set
):
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
2.3。數組流
數組也可以是Stream的來源:
Stream<String> streamOfArray = Stream.of("a", "b", "c");
也可以從現有陣列或陣列的一部分中創建它們:
String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
2.4。 Stream.builder()
使用builder時,**應在語句的右側另外指定所需的類型,**否則build()
方法將創建Stream<Object>:
的實例Stream<Object>:
Stream<String> streamBuilder =
Stream.<String>builder().add("a").add("b").add("c").build();
2.5。 Stream.generate()
**generate()
**方法接受Supplier<T>
進行元素生成。由於結果流是無限的,因此開發人員應指定所需的大小,否則generate()
方法將起作用直到達到內存限制:
Stream<String> streamGenerated =
Stream.generate(() -> "element").limit(10);
上面的代碼創建了一個由十個字符串組成的序列,其值是– “element”
。
2.6。 Stream.iterate()
創建無限流的另一種方法是使用**iterate()
**方法:
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
結果流的第一個元素是iterate()
方法的第一個參數。為了創建每個後續元素,將指定功能應用於上一個元素。在上面的示例中,第二個元素為42。
2.7。基本類型流
Java 8提供了從三種基本類型中創建流的可能性: int, long
和double.
由於Stream<T>
是泛型接口,無法將基元用作泛型的類型參數,因此創建了三個新的特殊接口: IntStream, LongStream, DoubleStream.
使用新接口可以減輕不必要的自動裝箱,從而提高生產率:
IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);
**range(int startInclusive, int endExclusive)
**方法創建從第一個參數到第二個參數的有序流。它以等於1的步長遞增後續元素的值。結果不包括最後一個參數,它只是序列的上限。
**rangeClosed(int startInclusive, int endInclusive)**
方法的作用相同,只是有一個區別–包含第二個元素。這兩種方法可用於生成三種基本類型的流中的任何一種。
從Java 8開始, Random
類為生成基元流提供了廣泛的方法。例如,以下代碼創建一個DoubleStream,
其中包含三個元素:
Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
2.8。流String
String
也可以用作創建流的源。
借助String
類的chars()
方法。由於沒有界面CharStream
在JDK的IntStream
用於表示字符流代替。
IntStream streamOfChars = "abc".chars();
下面的示例根據指定的RegEx
將String
分成子字符串:
Stream<String> streamOfString =
Pattern.compile(", ").splitAsStream("a, b, c");
2.9。文件流
Java NIO類Files
允許通過lines()
方法生成文本文件的Stream<String>
。文本的每一行都成為流的元素:
Path path = Paths.get("C:\\file.txt");
Stream<String> streamOfStrings = Files.lines(path);
Stream<String> streamWithCharset =
Files.lines(path, Charset.forName("UTF-8"));
可以將Charset
指定為lines()
方法的參數。
3.引用流
只要僅調用中間操作,就可以實例化流並對其進行可訪問的引用。執行中間操作使流不可訪問.
為了證明這一點,我們將忘記一段時間的最佳實踐是將操作順序鏈接在一起。除了不必要的冗長之外,從技術上講,以下代碼有效:
Stream<String> stream =
Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional<String> anyElement = stream.findAny();
但是,在調用中間操作之後嘗試重用相同的引用將觸發IllegalStateException:
Optional<String> firstElement = stream.findFirst();
由於IllegalStateException
是RuntimeException
,因此編譯器將不會知道提示該問題。因此,記住Java 8流不可重複使用這點非常重要。
這種行為是合乎邏輯的,因為流被設計為提供以功能樣式將有限的操作序列應用於元素源的功能,但不能存儲元素。
因此,為使先前的代碼正常工作,應進行一些更改:
List<String> elements =
Stream.of("a", "b", "c").filter(element -> element.contains("b"))
.collect(Collectors.toList());
Optional<String> anyElement = elements.stream().findAny();
Optional<String> firstElement = elements.stream().findFirst();
4.流管道
要對數據源的元素執行一系列操作並彙總其結果,需要三部分-源,中間操作和終端操作。
中間操作返回新的修改後的流。例如,要創建一個現有流的新流而跳過幾個元素,則應使用skip()
方法:
Stream<String> onceModifiedStream =
Stream.of("abcd", "bbcd", "cbcd").skip(1);
如果需要多個修改,則可以鏈接中間操作。假設我們還需要用前幾個字符的子字符串替換當前Stream<String>
每個元素。這將通過鏈接skip()和map()方法來完成:
Stream<String> twiceModifiedStream =
stream.skip(1).map(element -> element.substring(0, 3));
如您所見, map()
方法將lambda表達式作為參數。如果您想了解有關Lambda的更多信息,請查看我們的教程Lambda表達式和功能接口。
流本身是一文不值的,用戶真正感興趣的是終端操作的結果,該操作可以是某種類型的值或應用於流的每個元素的操作。每個流只能使用一個終端操作。
使用流的正確和最方便的方法是通過流管道,流管道是流源,中間操作和終端操作的鏈。例如:
List<String> list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
.map(element -> element.substring(0, 3)).sorted().count();
5.懶惰調用
中間操作是懶惰的。這意味著只有在執行終端操作時才需要調用它們。
為了證明這一點,假設我們有一個方法wasCalled()
,
它在每次調用時都會增加一個內部計數器:
private long counter;
private void wasCalled() {
counter++;
}
我們叫方法Called
()
從操作filter()
List<String> list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream<String> stream = list.stream().filter(element -> {
wasCalled();
return element.contains("2");
});
由於我們擁有三個元素的來源,因此可以假設方法filter()
將被調用3次,並且counter
變量的值將為3。但是運行此代碼完全不會更改counter
,它仍然為零,因此,甚至沒有一次調用filter()
方法。原因–缺少終端操作。
讓我們通過添加map()
操作和終端操作– findFirst().
稍微重寫一下這段代碼findFirst().
我們還將添加借助日誌記錄功能來跟踪方法調用順序的功能:
Optional<String> stream = list.stream().filter(element -> {
log.info("filter() was called");
return element.contains("2");
}).map(element -> {
log.info("map() was called");
return element.toUpperCase();
}).findFirst();
結果日誌顯示,兩次調用了filter()
方法,而一次調用了map()
方法。之所以如此,是因為管道是垂直執行的。在我們的示例中,流的第一個元素不滿足filter的謂詞,然後為第二個元素調用filter()
方法,第二個元素通過了filter。在不調用第三個元素的filter()
,我們通過了流水線到map()
方法。
findFirst()
操作僅滿足一個元素。因此,在此特定示例中,延遲調用允許避免兩種方法調用-一種用於filter()
,一種用於map().
6.執行命令
從性能的角度來看,正確的順序是流管道中鏈接操作最重要的方面之一:
long size = list.stream().map(element -> {
wasCalled();
return element.substring(0, 3);
}).skip(2).count();
執行此代碼將使計數器的值增加三。這意味著該流的map()
方法被調用了三次。但是size
的值是1。因此,結果流只有一個元素,並且我們無故執行了昂貴的map()
操作(三分之二)。
如果我們更改skip()
和map()
方法的順序,
則counter
只會增加1。因此,方法map()
將僅被調用一次:
long size = list.stream().skip(2).map(element -> {
wasCalled();
return element.substring(0, 3);
}).count();
這使我們達到了規則:減小流大小的中間操作應該放在應用於每個元素的操作之前。因此,將諸如s kip(), filter(), distinct()
類的方法保留在流管道的頂部。
7.流縮減機制
該API具有許多終端操作,這些操作將流聚合為一種類型或一種原語,例如count(), max(), min(), sum()
,
但是這些操作根據預定義的實現而工作。如果開發人員需要自定義Stream的縮減機制,該怎麼辦?有兩種方法可以執行此操作– **reduce()**
和**collect()
**方法。
7.1。 reduce()
方法
此方法有三種變體,它們的簽名和返回類型不同。它們可以具有以下參數:
身份–累加器的初始值或默認值(如果流為空且沒有要累加的值);
累加器–一個指定元素聚合邏輯的函數。當累加器為減少的每個步驟創建一個新值時,新值的數量等於流的大小,並且只有最後一個值才有用。這對性能不是很好。
組合器–匯總累加器結果的函數。只能在並行模式下調用合併器,以減少來自不同線程的累加器的結果。
因此,讓我們來看一下這三種方法:
OptionalInt reduced =
IntStream.range(1, 4).reduce((a, b) -> a + b);
reduced
= 6(1 + 2 + 3)
int reducedTwoParams =
IntStream.range(1, 4).reduce(10, (a, b) -> a + b);
reducedTwoParams
= 16(10 +1 + 2 + 3)
int reducedParams = Stream.of(1, 2, 3)
.reduce(10, (a, b) -> a + b, (a, b) -> {
log.info("combiner was called");
return a + b;
});
結果將與前面的示例(16)相同,並且沒有登錄名,這意味著未調用合併器。為了使組合器正常工作,流應該是並行的:
int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
.reduce(10, (a, b) -> a + b, (a, b) -> {
log.info("combiner was called");
return a + b;
});
這裡的結果是不同的(36),並且組合器被調用了兩次。這裡的歸約是通過以下算法進行的:累加器通過將流的每個元素添加到流的每個元素來identity
從而運行了3次。這些動作是並行進行的。結果,它們具有(10 +1 = 11; 10 + 2 = 12; 10 + 3 = 13;)。現在,合併器可以合併這三個結果。為此需要兩次迭代(12 + 13 = 25; 25 + 11 = 36)。
7.2。 collect()
方法
流的減少還可以通過另一個終端操作– collect()
方法來執行。它接受類型為Collector,
的參數,該參數指定減少的機制。已經為大多數常見操作創建了預定義的收集器。可以在Collectors
類型的幫助下訪問它們。
在本節中,我們將使用以下List
作為所有流的來源:
List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"), new Product(13, "lemon"),
new Product(23, "bread"), new Product(13, "sugar"));
將流轉換為Collection
( Collection,List或Set ):
List<String> collectorCollection =
productList.stream().map(Product::getName).collect(Collectors.toList());
簡化為String :
String listToString = productList.stream().map(Product::getName)
.collect(Collectors.joining(", ", "[", "]"));
joiner()
方法可以具有一到三個參數(定界符,前綴,後綴)。關於使用joiner()
的最方便的事情–開發人員無需檢查流是否到達其末尾即可應用後綴而不應用定界符。 Collector
會照顧的。
處理流中所有數字元素的平均值:
double averagePrice = productList.stream()
.collect(Collectors.averagingInt(Product::getPrice));
處理流中所有數字元素的總和:
int summingPrice = productList.stream()
.collect(Collectors.summingInt(Product::getPrice));
averagingXX(), summingXX()
和summarizingXX()
可以與基元( int, long, double
)及其包裝器類( Integer, Long, Double
)一起使用。這些方法的一個更強大的功能是提供映射。因此,開發人員無需在collect()
方法之前使用其他map()
操作。
收集有關流元素的統計信息:
IntSummaryStatistics statistics = productList.stream()
.collect(Collectors.summarizingInt(Product::getPrice));
通過使用結果類型為IntSummaryStatistics
實例,開發人員可以通過應用toString()
方法來創建統計報告。其結果將是一個String
共同這一個“IntSummaryStatistics{count=5, sum=86, min=13, average=17,200000, max=23}”.
通過應用方法getCount(), getSum(), getMin(), getAverage(), getMax().
從該對像中提取count, sum, min, average
的單獨值也很容易getCount(), getSum(), getMin(), getAverage(), getMax().
所有這些值都可以從單個管道中提取。
根據指定的功能對流元素進行分組:
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getPrice));
在上面的示例中,信息流被簡化為Map
,該Map
按價格將所有產品分組。
根據某些謂詞將流的元素分為幾類:
Map<Boolean, List<Product>> mapPartioned = productList.stream()
.collect(Collectors.partitioningBy(element -> element.getPrice() > 15));
推動收集器執行其他轉換:
Set<Product> unmodifiableSet = productList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
在這種特殊情況下,收集器已將流轉換為Set
,然後從中創建了不可修改的Set
。
定制收集器:
如果出於某種原因應該創建一個自定義收集器,那麼最簡單,最不繁瑣的方法是使用Collector.
類型of()
方法Collector.
Collector<Product, ?, LinkedList<Product>> toLinkedList =
Collector.of(LinkedList::new, LinkedList::add,
(first, second) -> {
first.addAll(second);
return first;
});
LinkedList<Product> linkedListOfPersons =
productList.stream().collect(toLinkedList);
在此示例中, Collector
的實例被簡化為LinkedList<Persone>.
並行流
在Java 8之前,並行化很複雜。 ExecutorService
和ForkJoin
簡化了開發人員的生活,但他們仍應牢記如何創建特定的執行程序,如何運行它等等。 Java 8引入了一種以功能樣式完成並行性的方法。
該API允許創建並行流,以並行模式執行操作。當流的源是Collection或數組時,可以藉助於**parallelStream()**方法來實現:
Stream<Product> streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
.map(product -> product.getPrice() * 12)
.anyMatch(price -> price > 200);
如果流的源與Collection
或array
,則應使用**parallel()
**方法:
IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();
在後台,Stream API自動使用ForkJoin
框架並行執行操作。默認情況下,將使用公共線程池,並且(目前至少)沒有辦法為其分配一些自定義線程池。這可以通過使用一組定制的並行收集器來克服。
在並行模式下使用流時,請避免阻塞操作,並在任務需要相似的時間來執行時使用並行模式(如果一個任務的持續時間比另一個任務長得多,則可能會減慢整個應用程序的工作流程)。
可以使用sequential()
方法將並行模式下的流轉換回順序模式:
IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();
結論
Stream API是一組功能強大但易於理解的工具,用於處理元素序列。如果使用得當,它可以減少大量的樣板代碼,創建更具可讀性的程序並提高應用程序的生產率。
在本文中顯示的大多數代碼示例中,流都未被消耗(我們未應用close()
方法或終端操作)。在真實的應用程序中,不要將實例化的流保留為未消耗狀態,因為這會導致內存洩漏。
本文附帶的完整代碼示例可在GitHub上獲得。