Java8 Stream流API使用簡介
概述
本文介紹Java8 Streams從創(chuàng)建到并行執(zhí)行的實際使用例子,涉及 Java8(lambda表達式、Optional、方法引用)和流API的基本知識。
流創(chuàng)建
有很多方法可以創(chuàng)建不同源的流實例。一旦創(chuàng)建,實例將不會修改其源,因此允許從單個源創(chuàng)建多個實例。
- 空流
Stream<String> streamEmpty = Stream.empty();
常在創(chuàng)建時使用empty方法,以避免對沒有元素的流返回null:
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
- 集合流
可以創(chuàng)建任何類型的集合(集合、列表、數(shù)組)的流:
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
Stream<String> streamOfArray = Stream.of("a", "b", "c");
還可以從現(xiàn)有數(shù)組或數(shù)組的一部分創(chuàng)建流:
String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
- Stream.builder()
當使用builder時,應該在語句的右側部分額外指定所需的類型,否則build方法將創(chuàng)建Stream<Object>的實例:
Stream<String> streamBuilder =
Stream.<String>builder().add("a").add("b").add("c").build();
- Stream.generate()
generate方法接受Supplier<T>來生成元素。由于生成的流是無限的,開發(fā)人員應該指定所需的大?。?/p>
Stream<String> streamGenerated =
Stream.generate(() -> "element").limit(10);
- Stream.iterate()
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
結果流的第一個元素是iterate方法的第一個參數(shù)。創(chuàng)建之后的每個元素時,指定的函數(shù)將應用于前一個元素。在上面的例子中,第二個元素將是42。
Primitives基元流
Java8提供了從三種基本類型創(chuàng)建流的可能性:int、long和double。由于Stream<T>是一個泛型接口,并且無法將基元用作泛型的類型參數(shù),因此創(chuàng)建了三個新的特殊接口:IntStream、LongStream和DoubleStream。
使用該接口可以減少不必要的自動裝箱,從而提高效率:
IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);
range(int startInclusive,int endExclusive)方法創(chuàng)建從第一個參數(shù)到第二個參數(shù)的有序流。它以等于1的步長遞增后續(xù)元素的值。結果不包括最后一個參數(shù),它只是序列的一個上界。
rangeClosed(int startInclusive,int endInclusive)方法執(zhí)行相同的操作,但只有一個區(qū)別,即包括第二個元素。我們可以使用這兩種方法來生成三種類型的基元流中的任何一種。
自Java 8以來,Random類提供了一系列用于生成基元流的方法。例如,以下代碼創(chuàng)建了一個DoubleStream,它有三個元素:
Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
- 字符串流
在String類的chars()方法的幫助下,我們還可以使用String作為創(chuàng)建流的源。由于JDK中沒有CharStream的接口,因此我們使用IntStream來表示字符流。
IntStream streamOfChars = "abc".chars();
以下示例根據(jù)指定的RegEx將字符串分解為子字符串:
Stream<String> streamOfString =
Pattern.compile(", ").splitAsStream("a, b, c");
- 文件流
此外,Java NIO類Files允許我們通過line()方法生成文本文件的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"));
引用流
記住Java 8流是不能重用的,這一點非常重要。
Stream<String> stream =
Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional<String> anyElement = stream.findAny();
Optional<String> firstElement = stream.findFirst();
嘗試重用相同的引用將觸發(fā)IllegalStateException:這種行為是合乎邏輯的。流的設計是為了以函數(shù)樣式將有限的操作序列應用于元素源,而不是存儲元素。
因此,為了使以前的代碼正常工作,應該進行一些更改:
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();
懶調用
使用流的正確和最方便的方法是通過流管道,它是流源、中間操作和終端操作的鏈:
List<String> list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
.map(element -> element.substring(0, 3)).sorted().count();
中間操作是惰性的。這意味著只有在終端操作執(zhí)行需要時才會調用它們。
例如,讓我們調用方法wasCalled(),它每次調用時都會增加一個內部計數(shù)器:
private long counter;
private void wasCalled() {
counter++;
}
現(xiàn)在,讓我們從操作filter()中調用方法wasCalled():
List<String> list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream<String> stream = list.stream().filter(element -> {
wasCalled();
return element.contains("2");
});
由于我們有三個元素的源,可以假設filter()方法將被調用三次,計數(shù)器變量的值將為3。然而,運行此代碼根本不會更改計數(shù)器,它仍然為零,因此filter()方法甚至沒有被調用過一次,缺少終端操作的原因。
讓我們通過添加map()操作和終端操作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()方法一次。這是因為管道是垂直執(zhí)行的。
在示例中,流的第一個元素不滿足過濾器的謂詞。然后,調用了第二個元素的filter()方法,通過管道進入map()方法,findFirst()操作只滿足一個元素,因此調用結束返回。
因此,在這個特定的例子中,惰性調用使我們能夠避免兩個方法調用,一個用于filter(),另一個用于map()。
執(zhí)行順序
從性能的角度來看,正確的順序是流管道中鏈操作最重要的方面之一:
long size = list.stream().map(element -> {
wasCalled();
return element.substring(0, 3);
}).skip(2).count();
執(zhí)行此代碼將使計數(shù)器的值增加3,這意味著我們調用了流的map()方法三次,但返回的值是1。因此,生成的流只有一個元素,而無緣無故地執(zhí)行了三次中的兩次昂貴的map()操作。
如果我們改變skip()和map()方法的順序,計數(shù)器將只增加一個。因此,我們將只調用map()方法一次:
long size = list.stream().skip(2).map(element -> {
wasCalled();
return element.substring(0, 3);
}).count();
這就引出了以下規(guī)則:減少流大小的中間操作應該放在應用于每個元素的操作之前。因此,我們需要將skip()、filter()和distinct()等方法保留在流管道的頂部。
reduce()流聚合
流API默認提供了一些流聚合的操作:count()、max(),min()和sum(),如果需要自定義聚合,可以使用reduce()和collect()。
reduce具有以下參數(shù):
- identity:累加器的初始值,如果流為空并且沒有任何可累加的內容,則為默認值;
- accumulator累加器:一個指定元素聚合邏輯的函數(shù)。由于累加器為每一個步驟創(chuàng)建一個新值,所以新值的數(shù)量等于流的大小,只有最后一個值是有用的。
- combiner組合器:一個聚合累加器結果的函數(shù)。只在并行模式下調用組合器。
現(xiàn)在,讓我們看看這三種方法的作用:
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,組合器被調用了兩次:流的每個元素添加到累加器運行三次,并且是并行進行的。因此,它們具有(10+1=11;10+2=12;10+3=13;)。現(xiàn)在組合器可以合并這三個結果。它需要兩次迭代(12+13=25;25+11=36)。
collect()收集器
流API已經(jīng)為大多數(shù)常見操作創(chuàng)建了預定義的收集器。
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"));
將流轉換為集合:
List<String> collectorCollection =
productList.stream().map(Product::getName).collect(Collectors.toList());
還原為字符串:
String listToString = productList.stream().map(Product::getName)
.collect(Collectors.joining(", ", "[", "]"));
joiner()方法可以有1到3個參數(shù)(分隔符、前綴、后綴)。
處理流中所有數(shù)字元素的平均值:
int summingPrice = productList.stream()
.collect(Collectors.summingInt(Product::getPrice));
方法averagingXX()、summingXX()和summaryzingXX()可以處理基元(int、long、double)及其包裝類(Integer、long、double)。這些方法的一個更強大的功能是提供映射。因此,開發(fā)人員不需要在collect()方法之前使用額外的map()操作。
IntSummaryStatistics statistics = productList.stream()
.collect(Collectors.summarizingInt(Product::getPrice));
結果將是一個與此“IntSummaryStatistics{count=5,sum=86,min=13,average=17,max=23}”相同的字符串
根據(jù)指定的函數(shù)對流的元素進行分組:
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getPrice));
根據(jù)一些謂詞將流的元素分組:
Map<Boolean, List<Product>> mapPartioned = productList.stream()
.collect(Collectors.partitioningBy(element -> element.getPrice() > 15));
推進收集器時可執(zhí)行附加轉換:
Set<Product> unmodifiableSet = productList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
如果出于某種原因應該創(chuàng)建自定義收集器,那么最簡單的方法是使用收集器類型的of()方法。
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);
并行流
Java 8引入了一種以函數(shù)風格實現(xiàn)并行的方法。API允許我們創(chuàng)建并行流,以并行模式執(zhí)行操作。當流的源是Collection或數(shù)組時,可以借助parallelStream()方法實現(xiàn):
Stream<Product> streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
.map(product -> product.getPrice() * 12)
.anyMatch(price -> price > 200);
如果流的源不是Collection或數(shù)組,則應使用parallel()方法:
IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();
在后臺,Stream API自動使用ForkJoin框架并行執(zhí)行操作。默認情況下,將使用公共線程池。
在并行模式下使用流時,請避免阻塞操作。當任務需要類似的執(zhí)行時間時,最好使用并行模式。如果一項任務的持續(xù)時間比另一項長得多,則可能會減慢整個應用程序的工作流程。
并行模式下的流可以使用sequencial()方法轉換回順序模式:
IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();
結論
流API是一套功能強大但易于理解的工具,用于處理元素序列。如果使用得當,它可以減少大量代碼,創(chuàng)建更可讀的程序,并提高應用程序的生產(chǎn)力。在應用程序中,不要讓實例化的流未被使用,避免導致內存泄漏。