文章目錄
集合優化了對象的存儲,而流和對象的處理有關。
利用流,無需迭代集合中的元素,就可以提取和操作它們。組合在一起,在流上形成一條操作管道。
lambda表達式、方法引用、流式編程結合使用。當 Lambda 表達式和方法引用(method references)和流一起使用的時候會讓人感覺自成一體。
聲明式編程(Declarative programming)是一種:聲明要做什麼,而非怎麼做的編程風格。正如我們在函數式編程中所看到的。
流式編程採用內部迭代,這是流式編程的核心特性之一。這種機制使得編寫的代碼可讀性更強,也更能利用多核處理器的優勢。通過放棄對迭代過程的控制,我們把控制權交給並行化機制。
另一個重要方面,流是懶加載的。這代表着它只在絕對必要時才計算。你可以將流看作“延遲列表”。由於計算延遲,流使我們能夠表示非常大(甚至無限)的序列,而不需要考慮內存問題。
1、JDK13中對流的介紹
Package java.util.stream
這個包中引入的關鍵抽象是流。類Stream、IntStream、LongStream和DoubleStream是對象和原始int、long和double類型上的流。流與集合在以下幾個方面不同:
- 沒有存儲。流不是存儲元素的數據結構;相反,它通過計算操作的管道傳遞來自源的元素,如數據結構、數組、生成器函數或I/O通道。
- Functional in nature 。流上的操作產生結果,但不修改其源。例如,對從集合中獲得的流進行過濾將產生一個沒有過濾元素的新流,而不是從源集合中刪除元素。
- Laziness-seeking。許多流操作,如過濾、映射或重複刪除,可以延遲實現,從而暴露了優化的機會。例如,“查找包含三個連續元音的第一個字符串”不需要檢查所有的輸入字符串。流操作分爲中間(流生產)操作和終端(值或副作用生產)操作。中間操作總是延遲的。
- 可能是無限的。集合的大小是有限的,而流則不必如此。諸如limit(n)或findFirst()之類的短路操作允許在有限時間內完成無限流上的計算。
- consumable。流的元素在流的生命週期中只訪問一次。與迭代器一樣,必須生成新的流來重新訪問源的相同元素。
流可以通過多種方式獲得。一些例子包括:
2、流支持
如何將一個全新的流的概念融入到現有類庫中呢?
接口部分怎麼改造呢?特別是涉及集合類接口的部分。如果你想把一個集合轉換爲流,直接向接口添加新方法會破壞所有老的接口實現類。
Java 8 採用的解決方案是:在接口中添加被 default(默認)修飾的方法。通過這種方案,設計者們可以將流式(stream)方法平滑地嵌入到現有類中。流方法預置的操作幾乎已滿足了我們平常所有的需求。流操作的類型有三種:
-
創建流
-
修改流元素(中間操作, Intermediate Operations)
-
消費流元素(終端操作, Terminal Operations)通常意味着收集流元素(通常是到集合中)。
3、流創建
Stream.of()
通過Stream().of()將一組元素轉化爲流
import java.util.stream.*;
public class StreamOf {
public static void main(String[] args) {
Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
.forEach(System.out::print);
System.out.println();
Stream.of(3.14159, 2.718, 1.618)
.forEach(System.out::println);
}
}
輸出結果:
每個集合都可以通過調用 stream() 方法來產生一個流。
import java.util.*;
import java.util.stream.*;
public class CollectionToStream {
public static void main(String[] args) {
Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
w.stream()
.map(x -> x + "")
.forEach(System.out::println);
}
}
range()
IntStream 類提供了 range() 方法用於生成整型序列的流。編寫循環時,這個方法會更加便利
import static java.util.stream.IntStream.*;
public class Ranges {
public static void main(String[] args) {
System.out.println(range(10, 20).sum());
Repeat.repeat(5, Run::run);
}
}
class Run {
public static void run() {
System.out.println("this is run");
}
}
class Repeat {
public static void repeat(int n, Runnable runnable) {
range(0, n).forEach(i -> runnable.run());
}
}
實用小功能 repeat() 可以用來替換簡單的 for 循環。代碼示例:
import static java.util.stream.IntStream.*;
public class Repeat {
public static void repeat(int n, Runnable action) {
range(0, n).forEach(i -> action.run());
}
}
static Stream generate(Supplier<? extends T> s)
返回一個無限連續的無序流,其中每個元素由提供的Supplier<? extends T>生成。這適用於生成常量流、隨機元素流等。
static <T> Stream<T> iterate(T seed,Predicate<? super T> hasNext,UnaryOperator<T> next)
Stream.iterate() 以種子(第一個參數)開頭,並將其傳給方法(第二個參數)。方法的結果將添加到流,並存儲作爲第一個參數用於下次調用 iterate(),依次類推。
以滿足給定的hasNext謂詞爲條件,將給定的下一個函數迭代應用於初始元素,從而返回一個連續的有序流。一旦hasNext謂詞返回false,流就終止。
Stream.iterate應該生成與對應的for循環生成的元素序列相同的元素序列:
for (T index=seed; hasNext.test(index); index = next.apply(index)) {
...
}
流的建造者模式
Interface Stream.Builder
在建造者設計模式(也稱構造器模式)中,首先創建一個 builder 對象,傳遞給它多個構造器信息,最後執行“構造”。Stream 庫提供了這樣的 Builder。
Arrays.stream()
Arrays 類中含有一個名爲 stream() 的靜態方法用於把數組轉換成爲流。
正則表達式
Java 8 在 java.util.regex.Pattern 中增加了一個新的方法 splitAsStream()。這個方法可以根據傳入的公式將字符序列轉化爲流。但是有一個限制,輸入只能是 CharSequence,因此不能將流作爲 splitAsStream() 的參數。
中間操作
跟蹤和調試
peek() 操作的目的是幫助調試。它允許你無修改地查看流中的元素。
流元素排序
Stream sorted()
返回由該流的元素組成的流,按自然順序排序。如果此流的元素不可比較,則使用java.lang。在執行終端操作時,可能會拋出ClassCastException。
對於有序流,排序是穩定的。對於無序流,沒有穩定性保證。
Stream sorted(Comparator<? super T> comparator)
返回一個由這個流的元素組成的流,根據提供的比較器進行排序。
對於有序流,排序是穩定的。對於無序流,沒有穩定性保證。
sorted()中可以傳入一個 Comparator 參數
sorted() 預設了一些默認的比較器.
也可以把 Lambda 函數作爲參數傳遞給 sorted().
移除元素
返回由該流的不同元素(根據Object.equals(Object))組成的流。
對於有序流,不同元素的選擇是穩定的(對於重複的元素,首先出現在相遇順序中的元素將被保留)。對於無序流,沒有穩定性保證。
Stream distinct()
返回一個由與給定謂詞匹配的流的元素組成的流。
Stream filter(Predicate<? super T> predicate)
應用函數到元素
- <R> Stream<R> map(Function<? super T,? extends R> mapper) 返回一個流,該流包含將給定函數應用於該流元素的結果。
將作用的結果放入Stream中,得到的是元素流的流,不關閉原來的流 - IntStream mapToInt(ToIntFunction<? super T> mapper) 返回一個IntStream,其中包含將給定函數應用於該流元素的結果。
- mapToLong(ToLongFunction):操作同上,但結果是 LongStream
- mapToDouble(ToDoubleFunction):操作同上,但結果是 DoubleStream。
在 map() 中組合流
flatMap() 做了兩件事:將產生流的函數應用在每個元素上(與 map() 所做的相同),然後將每個流都扁平化爲元素,因而最終產生的僅僅是元素。
每個被映射的流在其內容被放入該流之後關閉。(如果映射的流爲空,則使用空流)。所以,傳入的方法體返回也應當是流,因爲原來的流關閉了。
flatMap(Function):當 Function 產生流時使用。
flatMapToInt(Function):當 Function 產生 IntStream 時使用。
flatMapToLong(Function):當 Function 產生 LongStream 時使用。
flatMapToDouble(Function):當 Function 產生 DoubleStream 時使用。
Optional類
是否有某種對象,可作爲流元素的持有者,即使查看的元素不存在也能友好地提示我們(也就是說,不會發生異常)?
Optional 可以實現這樣的功能。一些標準流操作返回 Optional 對象,因爲它們並不能保證預期結果一定存在。包括:
-
findFirst() 返回一個包含第一個元素的 Optional 對象,如果流爲空則返回 Optional.empty
-
findAny() 返回包含任意元素的 Optional 對象,如果流爲空則返回 Optional.empty
-
max() 和 min() 返回一個包含最大值或者最小值的 Optional 對象,如果流爲空則返回 Optional.empty
-
reduce() 不再以 identity 形式開頭,而是將其返回值包裝在 Optional 中。(identity 對象成爲其他形式的
reduce() 的默認結果,因此不存在空結果的風險)
注意,空流是通過 Stream.empty() 創建的。如果你在沒有任何上下文環境的情況下調用 Stream.empty(),Java 並不知道它的數據類型;這個語法解決了這個問題。
便利函數
有許多便利函數可以解包 Optional ,這簡化了上述“對所包含的對象的檢查和執行操作”的過程:
- ifPresent(Consumer):當值存在時調用 Consumer,否則什麼也不做。
- orElse(otherObject):如果值存在則直接返回,否則生成 otherObject。
- orElseGet(Supplier):如果值存在則直接返回,否則使用 Supplier 函數生成一個可替代對象。
- orElseThrow(Supplier):如果值存在直接返回,否則使用 Supplier 函數生成一個異常。
創建Optional
當我們在自己的代碼中加入 Optional 時,可以使用下面 3 個靜態方法:
- empty():生成一個空 Optional。
- of(value):將一個非空值包裝到 Optional 裏。
- ofNullable(value):針對一個可能爲空的值,爲空時自動生成 Optional.empty,否則將值包裝在 Optional
中。
我們不能通過傳遞 null 到 of() 來創建 Optional 對象。最安全的方法是, 使用 ofNullable() 來優雅地處理 null。
Optional 對象操作
當我們的流管道生成了 Optional 對象,下面 3 個方法可使得 Optional 的後續能做更多的操作:
-
filter(Predicate):將 Predicate 應用於 Optional 中的內容並返回結果。當 Optional 不滿足Predicate 時返回空。如果 Optional 爲空,則直接返回。
-
map(Function):如果 Optional 不爲空,應用 Function 於 Optional中的內容,並返回結果。否則直接返回 Optional.empty。
-
flatMap(Function):同 map(),但是提供的映射函數將結果包裝在 Optional 對象中,因此 flatMap()不會在最後進行任何包裝。
以上方法都不適用於數值型 Optional。一般來說,流的 filter() 會在 Predicate 返回 false 時移除流元素。而 Optional.filter() 在失敗時不會刪除 Optional,而是將其保留下來,並轉化爲空。
同 map() 一樣 , Optional.map() 應用於函數。它僅在 Optional 不爲空時才應用映射函數,並將 Optional 的內容提取到映射函數。
同 Optional.map(),Optional.flatMap() 將提取非空 Optional 的內容並將其應用在映射函數。唯一的區別就是 flatMap() 不會把結果包裝在 Optional 中,因爲映射函數已經被包裝過了。
Optional 流
假設你的生成器可能產生 null 值,那麼當用它來創建流時,你會自然地想到用 Optional 來包裝元素
終端操作
這些操作接收一個流併產生一個最終結果;它們不會向後端流提供任何東西。因此,終端操作總是你在管道中做的最後一件事情。
轉化成數組
-
toArray():將流轉換成適當類型的數組。
-
toArray(generator):在特殊情況下,生成器用於分配自定義的數組存儲。
這組方法在流操作產生的結果必須是數組形式時很有用。假如我們想在流裏複用獲取的隨機數,可以將他們保存到數組中。
forEach
應用最終的操作
- forEach(Consumer):你已經看到過很多次 System.out::println 作爲 Consumer 函數
- forEachOrdered(Consumer): 保證 forEach 按照原始流順序操作。
第一種形式:顯式設計爲任意順序操作元素,僅在引入 parallel() 操作時纔有意義。parallel():可實現多處理器並行操作。實現原理爲將流分割爲多個(通常數目爲 CPU 核心數)並在不同處理器上分別執行操作。因爲我們採用的是內部迭代,而不是外部迭代,所以這是可能實現的。
import java.util.*;
import java.util.stream.*;
public class ForEach {
private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
public static IntStream rands() {
return Arrays.stream(rints);
}
static final int SZ = 14;
public static void main(String[] args) {
rands().limit(SZ).forEach(n -> System.out.format("%d ", n));
System.out.println();
rands().limit(SZ).parallel().forEach(n -> System.out.format("%d ", n));
System.out.println();
rands().limit(SZ)
.parallel()
.forEachOrdered(n -> System.out.format("%d ", n));
}
}
運行結果
收集
-
collect(Collector):使用 Collector 收集流元素到結果集合中。
-
collect(Supplier, BiConsumer, BiConsumer):同上,第一個參數 Supplier
創建了一個新結果集合,第二個參數 BiConsumer 將下一個元素包含到結果中,第三個參數 BiConsumer 用於將兩個值組合起來。
可以通過將集合的構造函數引用傳遞給 Collectors.toCollection(),從而構建任何類型的集合。
比如:.collect(Collectors.toCollection(TreeSet::new));
// streams/TreeSetOfWords.java
import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
public class TreeSetOfWords {
public static void
main(String[] args) throws Exception {
Set<String> words2 =
Files.lines(Paths.get("TreeSetOfWords.java"))
.flatMap(s -> Arrays.stream(s.split("\\W+")))
.filter(s -> !s.matches("\\d+")) // No numbers
.map(String::trim)
.filter(s -> s.length() > 2)
.limit(100)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(words2);
}
}
Files.lines() 打開 Path 並將其轉換成爲行流。下一行代碼將匹配一個或多個非單詞字符(\w+)行進行分割,然後使用 Arrays.stream() 將其轉化成爲流,並將結果扁平映射成爲單詞流。使用 matches(\d+) 查找並移除全數字字符串(注意,words2 是通過的)。接下來我們使用 String.trim() 去除單詞兩邊的空白,filter() 過濾所有長度小於3的單詞,緊接着只獲取100個單詞,最後將其保存到 TreeSet 中。
我們也可以在流中生成 Map。代碼示例:
// streams/MapCollector.java
import java.util.*;
import java.util.stream.*;
class Pair {
public final Character c;
public final Integer i;
Pair(Character c, Integer i) {
this.c = c;
this.i = i;
}
public Character getC() { return c; }
public Integer getI() { return i; }
@Override
public String toString() {
return "Pair(" + c + ", " + i + ")";
}
}
class RandomPair {
Random rand = new Random(47);
// An infinite iterator of random capital letters:
Iterator<Character> capChars = rand.ints(65,91)
.mapToObj(i -> (char)i)
.iterator();
public Stream<Pair> stream() {
return rand.ints(100, 1000).distinct()
.mapToObj(i -> new Pair(capChars.next(), i));
}
}
public class MapCollector {
public static void main(String[] args) {
Map<Integer, Character> map =
new RandomPair().stream()
.limit(8)
.collect(
Collectors.toMap(Pair::getI, Pair::getC));
System.out.println(map);
}
}
在 Java 中,我們不能直接以某種方式組合兩個流。所以這裏創建了一個整數流,並且使用 mapToObj() 將其轉化成爲 Pair 流。
在大多數情況下,你可以在 java.util.stream.Collectors尋找到你想要的預先定義好的 Collector。在少數情況下當你找不到想要的時候,你可以使用第二種形式的 collect()。
// streams/SpecialCollector.java
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
public static void main(String[] args) throws Exception {
ArrayList<String> words =
FileToWords.stream("Cheese.dat")
.collect(ArrayList::new,
ArrayList::add,
ArrayList::addAll);
words.stream()
.filter(s -> s.equals("cheese"))
.forEach(System.out::println);
}
}
組合所有流元素
- reduce(BinaryOperator):使用 BinaryOperator 來組合所有流中的元素。因爲流可能爲空,其返回值爲Optional。
- reduce(identity, BinaryOperator):功能同上,但是使用 identity 作爲其組合的初始值。因此如果流爲空,identity 就是結果。
- reduce(identity, BiFunction, BinaryOperator):這個形式更爲複雜(所以我們不會介紹它),在這裏被提到是因爲它使用起來會更有效。通常,你可以顯式地組合 map() 和 reduce() 來更簡單的表達它。
// streams/Reduce.java
import java.util.*;
import java.util.stream.*;
class Frobnitz {
int size;
Frobnitz(int sz) { size = sz; }
@Override
public String toString() {
return "Frobnitz(" + size + ")";
}
// Generator:
static Random rand = new Random(47);
static final int BOUND = 100;
static Frobnitz supply() {
return new Frobnitz(rand.nextInt(BOUND));
}
}
public class Reduce {
public static void main(String[] args) {
Stream.generate(Frobnitz::supply)
.limit(10)
.peek(System.out::println)
.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
.ifPresent(System.out::println);
}
}
Lambda 表達式中的第一個參數 fr0 是上一次調用 reduce() 的結果。而第二個參數 fr1 是從流傳遞過來的值。
reduce() 中的 Lambda 表達式使用了三元表達式來獲取結果,當其 size 小於 50 的時候獲取 fr0 否則獲取序列中的下一個值 fr1。因此你會取得第一個 size 小於 50 的 Frobnitz,只要找到了就這個結果就會緊緊地攥住它,即使有其他候選者出現。雖然這是一個非常奇怪的約束,但是它確實讓你對 reduce() 有了更多的瞭解。
匹配
-
allMatch(Predicate) :如果流的每個元素根據提供的 Predicate 都返回 true 時,結果返回爲
true。這個操作將會在第一個 false 之後短路;也就是不會在發生 false 之後繼續執行計算。 -
anyMatch(Predicate):如果流中的任意一個元素根據提供的 Predicate 返回 true 時,結果返回爲
true。這個操作將會在第一個 true 之後短路;也就是不會在發生 true 之後繼續執行計算。 -
noneMatch(Predicate):如果流的每個元素根據提供的 Predicate 都返回 false 時,結果返回爲
true。這個操作將會在第一個 true 之後短路;也就是不會在發生 true 之後繼續執行計算。
import java.util.function.BiPredicate;
interface Matcher extends BiPredicate<Stream<Integer>,Predicate<Integer>> {}
public class Matching {
static void show(Matcher match, int val) {
System.out.println(match.test(IntStream
.rangeClosed(1, 9).boxed().peek(n -> System.out.format("%d ", n)),
n -> n < val));
}
public static void main(String[] args) {
show(Stream::allMatch, 10);
show(Stream::allMatch, 4);
show(Stream::anyMatch, 2);
show(Stream::anyMatch, 0);
show(Stream::noneMatch, 5);
show(Stream::noneMatch, 0);
}
}
BiPredicate 是一個二元謂詞,這意味着它只能接受兩個參數並且只返回 true 或者 false。它的第一個參數是我們要測試的流,第二個參數是一個謂詞 Predicate。因爲 Matcher 適用於所有的 Stream::*Match 方法形式,所以我們可以傳遞每一個到 show() 中。match.test() 的調用會被轉換成 Stream::*Match 函數的調用。
show() 獲取兩個參數,Matcher 匹配器和用於表示謂詞測試 n < val 中最大值的 val。這個方法生成一個從 1 到 9 的整數流。peek() 是用於向我們展示測試在短路之前的情況。你可以在輸出中發現每一次短路都會發生。
元素查找
-
findFirst():返回一個含有第一個流元素的 Optional,如果流爲空返回 Optional.empty。
-
findAny(:返回含有任意流元素的 Optional,如果流爲空返回 Optional.empty。
// streams/SelectElement.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class SelectElement {
public static void main(String[] args) {
System.out.println(rands().findFirst().getAsInt());
System.out.println(
rands().parallel().findFirst().getAsInt());
System.out.println(rands().findAny().getAsInt());
System.out.println(
rands().parallel().findAny().getAsInt());
}
}
findFirst() 無論流是否爲並行化的,總是會選擇流中的第一個元素。對於非並行流,findAny()會選擇流中的第一個元素(即使從定義上來看是選擇任意元素)。在這個例子中,我們使用 parallel() 來並行流從而引入 findAny() 選擇非第一個流元素的可能性。
如果必須選擇流中最後一個元素,那就使用 reduce()
// streams/LastElement.java
import java.util.*;
import java.util.stream.*;
public class LastElement {
public static void main(String[] args) {
OptionalInt last = IntStream.range(10, 20)
.reduce((n1, n2) -> n2);
System.out.println(last.orElse(-1));
// Non-numeric object:
Optional<String> lastobj =
Stream.of("one", "two", "three")
.reduce((n1, n2) -> n2);
System.out.println(
lastobj.orElse("Nothing there!"));
}
}
信息
- count():流中的元素個數。
- max(Comparator):根據所傳入的 Comparator 所決定的“最大”元素。
- min(Comparator):根據所傳入的 Comparator 所決定的“最小”元素。
數字流信息
- average() :求取流元素平均值。
- max() 和 min():因爲這些操作在數字流上面,所以不需要 Comparator。
- sum():對所有流元素進行求和。
- summaryStatistics():生成可能有用的數據。目前還不太清楚他們爲什麼覺得有必要這樣做,因爲你可以使用直接的方法產生所有的數據。
// streams/NumericStreamInfo.java
import java.util.stream.*;
import static streams.RandInts.*;
public class NumericStreamInfo {
public static void main(String[] args) {
System.out.println(rands().average().getAsDouble());
System.out.println(rands().max().getAsInt());
System.out.println(rands().min().getAsInt());
System.out.println(rands().sum());
System.out.println(rands().summaryStatistics());
}
}
運行結果
507.94
998
8
50794
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}