寫在前面
如果說函數式接口和lambda表達式是Java中函數式程式設計的基石,那麼stream就是在基石上的最富麗堂皇的大廈。
隻有熟悉了stream,你才能說熟悉了Java 的函數式程式設計。
本文主要介紹Stream的基礎概念和基本操作,讓大家對Stream有一個初步的了解。
本文的示例代碼可從gitee上擷取:https://gitee.com/cnmemset/javafp
stream的概念
首先,看一個典型的stream例子:
public static void simpleStream() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
int letterCount = words.stream()
.filter(s -> s.length() > 3) // 過濾掉長度小于等于3的單詞
.mapToInt(String::length) // 将每個單詞映射為單詞長度
.sum(); // 計算總長度 5(hello) + 5(world) + 4(love) = 14
// 輸出為 14
System.out.println(letterCount);
}
在上述例子中,我們将字元串清單 words 作為stream的資料源,然後執行了 filter-map-reduce 的系列操作(sum方法屬于 reduce 操作),後面會詳細介紹map和reduce 操作。如果你有大資料的程式設計經驗,會更容易了解map和reduce的含義。
stream的定義比較晦澀,大緻可以了解為是一個支援串行或并行操作的資料元素序列。它具備以下幾個特點:
- 首先,stream不是一種資料結構,它并不存儲資料。stream是某個資料源之上的資料視圖。資料源可以是一個數組,或者是一個Collection類,甚至還可以是I/O channel。它通過一個計算管道(a pipeline of computational operations),對資料源的資料進行filter-map-reduce的操作。
- 其次,stream天生支援函數式程式設計。函數式程式設計的一個重要特點就是不會修改變量的值(沒有“副作用”)。而對stream的任何操作,都不會修改資料源中的資料。例如,對一個資料源為Collection的stream進行filter操作,隻會生成一個新的stream對象,而不會真的删除底層資料源中的元素。
- 第三,stream的許多操作都是惰性求值的(laziness-seeking)。惰性求值是指該操作隻是對stream的一個描述,并不會馬上執行。這類惰性的操作在stream中被稱為中間操作(intermediate operations)。
- 第四,stream呈現的資料可以是無限的。例如Stream.generate可以生成一個無限的流。我們可以通過 limit(n) 方法來将一個無限流轉換為有限流,或者通過 findFirst() 方法終止一個無限流。
- 最後,stream中的元素隻能被消費1次。和疊代器 Iterator 相似,當需要重複通路某個元素時,需要重新生成一個新的stream。
stream的操作可以分成兩類,中間操作(intermediate operations)和終止操作(terminal operations)。一個stream管道(stream pipeline)是由一個資料源 + 0個或多個中間操作 + 1個終止操作組成的。
中間操作:
中間操作(intermediate operations)指的是将一個stream轉換為另一個stream的操作,譬如filter和map操作。中間操作都是惰性的,它們的作用僅僅是描述了一個新的stream,不會馬上被執行。
終止操作:
終止操作(terminal operations)則指的是那些會産生一個新值或副作用(side-effect)的操作,譬如count 和 forEach 操作。隻有遇到終止操作時,之前定義的中間操作才會真正被執行。需要注意,當一個stream執行了一個終止操作後,它的狀态會變成“已消費”,不能再被使用。
為了證明“中間操作都是惰性的”,我們設計了一個實驗性的示例代碼:
public static void intermediateOperations() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
System.out.println("start: " + System.currentTimeMillis());
Stream<String> interStream = words.stream()
.filter(s -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
return s.length() > 3;
});
IntStream intStream = interStream.mapToInt(s -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
return s.length();
});
// 因為 filter 和 map 操作都屬于中間操作,并不會真正執行,
// 是以它們不受 Thread.sleep 的影響,耗時很短
System.out.println("after filter && map: " + System.currentTimeMillis());
int letterCount = intStream.sum();
// sum 屬于終止操作,會執行之前定義的中間操作,
// Thread.sleep 被真正執行了,耗時為 5(filter) + 3(mapToInt) = 8秒
System.out.println("after sum: " + System.currentTimeMillis());
// 輸出為 14
System.out.println(letterCount);
}
上述代碼的輸出類似:
start: 1633438922526
after filter && map: 1633438922588
after sum: 1633438930620
14
可以看到,上述代碼驗證了“中間操作都是惰性的”:列印“start”和列印“after filter && map”之間隻隔了幾十毫秒,而列印“after sum”則在8秒之後,證明了隻有在遇到 sum 操作後,filter 和 map 中定義的函數才真正被執行。
生成一個stream對象
Java 8中,引入了4個stream的接口:Stream、IntStream、LongStream、DoubleStream,分别對應Object類型,以及基礎類型int、long和double。如下圖所示:

在Java中,與stream相關的操作基本都是通過上述的4個接口來實作的,不會涉及到具體的stream實作類。要得到一個stream,通常不會手動建立,而是調用對應的工具方法。
常用的工具方法包括:
- Collection方法:Collection.stream() 或 Collection.parallelStream()
- 數組方法:Arrays.stream(Object[])
- 工廠方法:Stream.of(Object[]), IntStream.range(int, int) 或 Stream.iterate(Object, UnaryOperator) 等等
- 讀取檔案方法:BufferedReader.lines()
- 類 java.nio.file.Files 中,也提供了Stream相關的API,例如 Files.list, Files.walk 等等
Stream的基本操作
我們以接口Stream為例,先介紹stream的一些基本操作。
forEach()
Stream中的forEach方法和Collection中的forEach方法相似,都是對每個元素執行指定的操作。
forEach方法簽名為:
void forEach(Consumer<? super T> action)
forEach方法是一個終止操作,意味着在它之前的所有中間操作都将會被執行,然後再馬上執行 action 。
filter()
filter方法的方法簽名是:
Stream<T> filter(Predicate<? super T> predicate)
filter方法是一個中間操作,它的作用是根據參數 predicate 過濾元素,傳回一個隻包含滿足predicate條件元素的Stream。
示例代碼:
public static void filterStream() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
words.stream()
.filter(s -> s.length() > 3) // 過濾掉長度小于等于3的單詞
.forEach(s -> System.out.println(s));
}
上述代碼輸出為:
hello
world
love
limit()
limit方法簽名為:
Stream<T> limit(long maxSize);
limit方法是一個短路型(short-circuiting)的中間操作,作用是将目前的Stream截斷,隻留下最多 maxSize 個元素組成一個新的Stream。短路型(short-circuiting)的含義是指将一個無限元素的Stream轉換為一個有限元素的Stream。
例如,Random.ints 可以生成一個近似無限的随機整數流,我們可以通過limit方法限制生成随機整數的個數。示例代碼:
public static void limitStream() {
Random random = new Random();
// 列印左閉右開區間中 [1, 100) 中的 5 個随機整數
random.ints(1, 100)
.limit(5)
.forEach(System.out::println);
}
90
31
31
52
63
distinct()
distinct的方法簽名是:
Stream<T> distinct();
distinct是一個中間操作,作用是傳回一個去除重複元素後的Stream。
作者曾遇到過一個有趣的場景:要生成10個不重複的随機數字。可以結合Random.ints (Random.ints 可以生成一個近似無限的随機整數流)方法來實作這個需求。示例代碼如下:
public static void distinctStream() {
Random random = new Random();
// 在左閉右開區間中 [1, 100) 随機生成 10 個不重複的數字
random.ints(1, 100)
.distinct()
.limit(10)
.forEach(System.out::println);
/*
// 一個有趣的問題,如果 limit 方法放在 distinct 前面,
// 結果和上面的代碼有什麼差別嗎?
// 歡迎加群讨論。
random.ints(1, 100)
.limit(10)
.distinct()
.forEach(System.out::println);
*/
}
sorted()
sorted的方法簽名有兩個,分别是:
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
前者是按照自然順序排序,後者是根據指定的比較器進行排序。
sorted方法是一個中間操作,和Collection.sort方法作用相似。
示例代碼如下:
public static void sortedStream() {
List<String> list = Arrays.asList("Guangdong", "Fujian", "Hunan", "Guangxi");
// 自然排序
list.stream().sorted().forEach(System.out::println);
System.out.println("===============");
// 對省份進行排序,首先按照長度排序,如果長度一樣,則按照字母順序排序
list.stream().sorted((first, second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}).forEach(System.out::println);
}
上述代碼的輸出為:
Fujian
Guangdong
Guangxi
Hunan
===============
Hunan
Fujian
Guangxi
Guangdong
結語