熟悉 ES6 的開發者,肯定對數組的一些方法不是很陌生:map
、filter
等。在對一組對象進行統一操作時,利用這些方法寫出來的代碼比常規的迭代代碼更加的簡練。在 C♯ 中,有 LINQ 來實現。那么在 Java 中有這樣的操作嗎?答案是有的,Java8 中引入了大量新特性,其中一個就是 Java 的流式 API。
在 Java 8 中,流(Stream
)與迭代器類似,都是用來對集合內的元素進行某些操作。它們之間最大的差別,是對迭代器的每個操作都會即時生效,而對流的操作則不是這樣。流的操作有兩種,中間操作和終止操作。對於中間操作並不會立即執行,只有當終止操作執行時,前面的中間操作才會一並執行(稱之為惰性求值)。對於某些復雜操作,流的效率會比傳統的迭代器要高。
注意:本文所講述的“流”不是 XXXInputStream
、XXXOutputStream
預備知識:lambda 表達式、Functional Interface
Functional Interface
在 Java8 中,新加入了一個注解:@FunctionalInterface
,用於標記一個接口是函數接口(即有且只有一個方法(不包括那些有默認實現的方法和標記為 static
的方法))。一個典型的例子就是 Java 中用於多線程的 Runnable
接口:
@FunctionalInterface
public interface Runnable {
void run();
}
另外一個例子來自於 Java8 中預定義的一些接口(位於 java.util.function
包下)
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
如果自己定義函數式接口,@FunctionalInterface
注解是可選的,只要接口內除靜態方法和有默認實現的方法之外有且只有一個方法,那么這個接口就被認為是 Functional Interface。
lambda 表達式
lambda 表達式是 Java 8 中新引進的語法糖,主要作用是快速定義一個函數(或一個方法)。其基本語法如下:
(參數列表) -> { 表達式內容 }
其中參數列表內,每個參數的類型是可選的,如果參數列表內沒有參數,或參數不止一個時,需要用 ()
進行占位。
lambda 表達式的主要作用,就是用於簡化代碼。熟悉 Java GUI 的讀者知道,之前要給一個控件添加事件響應的時候,我們通常是使用匿名內部類進行處理:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 這里處理事件響應的代碼
}
});
顯然,這種寫法是比較麻煩的,我們觀察上面的代碼,可以看到 ActionListener
中只有一個方法。控件的 addActionListener
實際上接受的是一個方法作為參數,事件發生時調用該方法作為響應。lambda 表達式的作用就是用於快速定義方法,於是可以對上面的方法改寫成如下形式
button.addActionListener(e -> {
// 處理事件響應
})
可以看到,引入 lambda 表達式后,整個方法都變得十分簡潔。這就是 lambda 表達式的作用。
基本使用
打開流
可以用如下方法打開一個 Stream
:
- 使用
Collection
子類的stream()
(串行流)或parallelStream()
- 使用
Arrays.stream()
方法為數組創建一個流 - 使用
Stream.of()
方法創建流 - 使用
Stream.iterate()
方法創建流 - 使用
Stream.generate()
方法創建流
其中前三種創建的流是有限流(里面的元素數量是有限個,因為創建該流的集合內元素數量也是有限的),后兩種創建的流是無限流(里面的元素是由傳入的參數進行生成的,具體可參閱 API 文檔)
對流進行操作
前文說過,流的操作有兩種:中間操作和終止操作。辨別這兩種操作的方法很簡單:觀察這些操作的返回值。如果方法的返回值是 Stream<T>
說明操作返回的是流自身,可以進行下一步操作,這一操作為中間操作,反之則為終止操作,終止操作結束后流即失效,想再次使用則需要創建新的流。
下面列舉一些(至少我比較經常用到的)一些流的操作
操作 | 描述 |
---|---|
<R> Stream<R> map(Function<? super T, ? extends R> mapper) |
將流里面的每個元素通過 mapper 轉換為另一個元素,並生成一個對應類型的流 |
Stream<T> filter(Predicate<? super T> predicate) |
從流里挑出所有符合 predicate 條件的所有元素,並放入一個新的流中 |
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) |
將流里面的每個元素“展開”,形成一個新的流(通常用於展開嵌套的 List 或數組(把矩陣轉換為數組之類的)) |
Optional<T> reduce(BinaryOperator<T> accumulator) T reduce(T identity, BinaryOperator<T> accumulator) |
常見的應用場景:求和。簡單來說就是對流內的每個元素進行一次操作,最后得到一個結果 |
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) <R, A> collect<Collector<? super T, A, R> collector |
常見的應用場景:把流中的元素收集到一個 List 中 |
boolean allMatch(Predicate<? super T> predicate) boolean anyMatch(Predicate<? super T> predicate) |
判斷流中是否所有元素(存在元素)滿足 predicate 判斷條件 |
以上僅展示了部分常用操作,其余操作可參見 Stream 類的 API 文檔,另外不要被 API 的參數嚇到。這些參數實際上大部分是來自於 java.util.function 的接口,且均為前文所說的 Functional Interface,所以實際使用時,我們都是傳遞 lambda 表達式給參數。
舉例
對於選擇題來說,其選項可以由以下結構表示
class Question {
String body;
List<Option> options;
// 省略 getter/setter
}
class Option {
String answer;
boolean right;
// 省略 getter/setter
}
假如我們有一個選擇題的題庫,要往里面添加一道選擇題,要求在插入前要進行判斷,說每個題目必須有至少一個正確答案,則可以這樣寫:
boolean isValidQuestion(Question question) {
return question.getOptions.stream().anyMatch(option -> option.isRight());
}
再舉一個例子,已知 Date
類有一個 toInstant()
方法可以將 Date
轉化為 Instant
,現有一個 List<Date>
的變量 dates
,想將其轉化為 List<Instant>
類型,可以這樣寫:
dates.stream().map(Date::toInstant).collect(Collectors.toList());
目前我遇到的操作大致就這些,之后遇到實際的例子會繼續添加到本文。