Java - Lambda 表達式與 Stream 接口
sschrodinger
2019/10/28
引用
深入理解 Java Stream 流水線 - CarpenterLee
Lambda 表達式
lamda 表達式提供了豐富的語法糖,簡化自己的程序,并優化自己的程序速度。
Lambda 實例
Lambda 表達式僅支持函數式編程接口,所謂函數式接口,指的是只有一個抽象方法的接口。
函數式接口可以被隱式轉換為 Lambda 表達式。函數式接口可以用 @FunctionalInterface
注解標識。
Java 中有大量的函數式編程接口,如 Runnable
、Callable
等。
JDK 1.8 之后,又添加了一個 Java 包存放函數式接口,如下:
// java.util.function.*
public interface Consumer<T>{
/**
* 函數式接口,無返回值,接受一個參數,代表一個消費者
**/
void accept(T t);
// ...
}
public interface Supplier<T>{
/**
* 函數式接口,有返回值,無參數,代表一個生產者
**/
T get();
}
public interface Function<T, R>{
/**
* 函數式接口,有返回值,有參數
**/
R apply(T t);
// ...
}
// ...
舉個例子,當我們需要創建一個線程時,標準的寫法如下(使用匿名內部類):
public class Demo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello,world");
}
}).start();
}
}
lambda 表達式可以更加簡略的寫代碼,如上代碼的等效 lambda 表達式如下:
public class Demo {
public static void main(String[] args) {
new Thread
(
() -> { System.out.println("hello,world");}
).start();
}
}
Lambda 語法
在 Java 中,使用 (paramters) -> expression;
或者 (paramters) -> {statements;}
來定義 lambda 表達式。其中,有部分寫法可以省略,規則如下:
note
- 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
- 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
- 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
- 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
lambda 表達式在只有一條代碼時還可以引用其他方法或構造器并自動調用,可以省略參數傳遞,代碼更加簡潔,引用方法的語法需要使用 ::
符號。lambda 表達式提供了四種引用方法和構造器的方式:
- 引用對象的方法 類::實例方法
- 引用類方法 類::類方法
- 引用特定對象的方法 特定對象::實例方法
- 引用類的構造器 類::new
最常見的方法,比如說遍歷打印字符串或者排序,如下:
public class LambdaDemo {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
list.add("1");
list.add("2");
list.sort(String::compareTo);
list.forEach(System.out::println);
}
}
其中 forEach
的實現如下:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
復合 lambda 表達式
實現原理
lambda 表達式是通過 MethodHandler
和 invokeDynamic
實現的。
Stream 接口
stream 接口主要是為了快速處理一些聚合操作,如下一個實實例:
返回以字符 A 開頭的字符串的最長長度。
最傳統的寫法如下:
public long maxLength(List<String> data) {
List<String> list = new LinkedList<>();
for (String s : data) {
if (s.startsWith("A")) list.add(s);
}
long max = -1;
for (String s: list) {
if (max < s.length()) max = s.length();
}
return max;
}
通過兩次循環獲得最大的值,但是,這樣寫因為有兩次循環,所以會有效率上的損失,改進寫法如下:
public long maxLength(List<String> data) {
long max = -1;
for (String s: data) {
if (s.startsWith("A")) {
if (max < s.length()) max = s.length();
}
}
return max;
}
Stream 接口等效于第二種方式,在一次迭代中盡可能多的執行用戶指定的操作,避免多次循環導致的效率問題。如下:
public long maxLength(List<String> data) {
return data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max().getAsLong();
}
Stream 接口總共分為兩種操作,一種為中間操作(Intermediate operations),一種為結束操作(Terminal operations)。
中間操作具體分為兩種,為不帶狀態的中間操作(Stateless)和帶狀態的中間操作(Stateful)。不帶狀態的中間操作指元素的處理不受到前面元素的影響,而帶狀態的中間操作必須要等到所有元素處理完之后才能知道結果。
結束操作也分為兩種,為短路操作和非短路操作,短路操作是指不用處理全部元素就可以返回結果,比如找到第一個滿足條件的元素。非短路操作要找到所有滿足條件的元素才能完成。
如下:
Stream 操作分類
|
|---中間操作(Intermediate operations)
| |
| |---無狀態(Stateless)
| | |---unordered() // 返回一個無序的流
| | |---filter() // 按照條件過濾
| | |---map() // 處理流,產生新流
| | |---mapToInt() // 獲得一個整數流
| | |---mapToLong() // 獲得一個長整型流
| | |---mapToDouble() // 獲得一個雙實數流
| | |---flatMap() // 扁平化流
| | |---flatMapToInt() // 扁平化流到整形
| | |---flatMapToLong() // 扁平化流到長整形
| | |---flatMapToDouble() // 扁平化流到雙實數類型
| | |---peek() // 沒有返回值的 map
| |
| |---有狀態(Stateful)
| |---distinct() // 實現非重復流
| |---sorted() // 排序
| |---limit() // 取前 x 元素
| |---skip() // 跳過前 x 元素
|
|
|---結束操作(Terminal operations)
|
|---非短路操作
| |---forEach() // 遍歷(使用 parallelStream 不保證順序)
| |---forEachOrdered() // 遍歷(保證順序)
| |---toArray() // 轉換成數組
| |---reduce() // 合并流,比如說對流中所有元素求和
| | data.stream().reduce((a, b) -> a + b);
| |---collect() // 收集流
| |---max() // 求最大值
| |---min() // 求最小值
| |---count() // 統計個數
|
|---短路操作(short-circuiting)
|---anyMatch() // 任意一個匹配,返回 true
|---allMatch() // 所有匹配,返回 true
|---noneMatch() // 所有都不匹配,返回 true
|---findFirst() // 找到第一個出現的元素并返回
|---findAny() // 找到任意出現的元素并返回
Stream 接口實現原理
Stream 接口采用流水線的方式執行整個過程,要構建一個流水線,需要解決三個問題:
- 流水線構建
- 流水線方法保存
- 流水線操作執行
- 流水線結果保存
流水線構建 使用 pipeline,Java Stream 中,流水線的中間節點使用 PipelineHelper
來代表一個 stage,每個 stage 就是一個用戶指定的操作,如 map
、filter
等。多個不同的 stage 連接成一個雙向鏈表,這個雙向鏈表就封裝了用戶的全部操作。
如下實例:
data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max();
構成的流水線如下:
|---------| ----> |---------| -----> |---------| ------> |---------|
| Head | |filter op| | map op | | max op |
|---------| <---- |---------| <----- |---------| <------ |---------|
data source ---------> stage 0 ---------> stage 1 ---------> stage 2 ---------> stage 3
stream() filter() mapToLong() max()
流水線方法保存 采用的是 sink
接口,提供了最重要的四個函數,如下:
interface Sink<T> extends Consumer<T> {
// 開始遍歷元素之前調用該方法,通知Sink做好準備。
default void begin(long size) {}
// 所有元素遍歷完成之后調用,通知Sink沒有更多的元素了。
default void end() {}
// 是否可以結束操作,可以讓短路操作盡早結束。
default boolean cancellationRequested() {
return false;
}
// 遍歷元素時調用,接受一個待處理元素,并對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法里,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。
default void accept(int value) {
throw new IllegalStateException("called wrong accept method");
}
default void accept(long value) {
throw new IllegalStateException("called wrong accept method");
}
default void accept(double value) {
throw new IllegalStateException("called wrong accept method");
}
// ...
}
一般來說,重寫這些函數就可以實現聚合邏輯。例如 filter
實現如下:
@Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
// 自己開始的同時,激活下一鏈
@Override
public void begin(long size) {
downstream.begin(-1);
}
// 預測是否滿足要求,滿足要求則將其丟給下一鏈處理
@Override
public void accept(P_OUT u) {
if (predicate.test(u))
downstream.accept(u);
}
};
}
};
}
對于稍微復雜的有狀態操作,如排序,如下:
private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
private ArrayList<T> list;
// 創建一個臨時的鏈表,保存結果
@Override
public void begin(long size) {
if (size >= Nodes.MAX_ARRAY_SIZE)
throw new IllegalArgumentException(Nodes.BAD_SIZE);
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
// 對臨時表進行排序
// 激活下一鏈處理
// 發送數據到下一鏈
@Override
public void end() {
list.sort(comparator);
downstream.begin(list.size());
if (!cancellationWasRequested) {
list.forEach(downstream::accept);
}
else {
for (T t : list) {
if (downstream.cancellationRequested()) break;
downstream.accept(t);
}
}
downstream.end();
list = null;
}
// 上一連將其所有的元素加入到臨時表中
@Override
public void accept(T t) {
list.add(t);
}
// ...
}
note
- 從流水線的執行上來看,對于無狀態的中間操作,是可以通過流水線進行并行的,但是對于有狀態的操作,則必須等到所有的數據處理完之后才能夠進行下一步,所以,每當有有狀態的中間操作,就會多一次循環。
以 filter
函數為例,函數主題返回一個實現了 Stream
接口的 StatelessOp
對象,如下:
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) // ... 匿名類實現
}
StatelessOp
是屬于 ReferencePipeline
的一個靜態內部類。構造函數如下:
// 將上一級 stage 引用加入到該 stage 中
StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
StreamShape inputShape,
int opFlags) {
super(upstream, opFlags);
assert upstream.getOutputShape() == inputShape;
}
那么實際上對于 filter
函數,是將上一級的 stage 引用(他自身)加入到了一個新的 stage 中。并返回了那個新的 stage,如下:
|--------------------------|
|---------| | previousStage = stage n |
| stage n | | stage n + 1 |
|---------| |--------------------------|
stream().func() ------------> stream.func().filter() ---------------------------->
流水線執行 使用結束操作喚醒。結束操作也是一個特殊的 stage。根據流水線的雙向隊列,不考慮結束操作時的執行,如下:
data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max();
// |------|>|----------------------------|>|-----------------------|>|------|
// | Head | | filter op | | map op | |max op|
// |------|<|----------------------------|<|-----------------------|<|------|
當執行 max
函數時,會執行如下的語句:
@Override
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
return reduce(BinaryOperator.maxBy(comparator));
}
maxBy
函數很簡單,僅僅是返回一個比較器,如下:
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
reduce
函數才是一個短路操作,如下:
@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
return evaluate(ReduceOps.makeRef(accumulator));
}
makeRef
函數本質上來說也是生成一個 sink
,如下:
public static <T> TerminalOp<T, Optional<T>> makeRef(BinaryOperator<T> operator) {
class ReducingSink implements AccumulatingSink<T, Optional<T>, ReducingSink> {
private boolean empty;
private T state;
public void begin(long size) {
empty = true;
state = null;
}
@Override
public void accept(T t) {
if (empty) {
empty = false;
state = t;
} else {
state = operator.apply(state, t);
}
}
@Override
public Optional<T> get() {
return empty ? Optional.empty() : Optional.of(state);
}
@Override
public void combine(ReducingSink other) {
if (!other.empty)
accept(other.state);
}
}
return new ReduceOp<T, Optional<T>, ReducingSink>(StreamShape.REFERENCE) {
@Override
public ReducingSink makeSink() {
return new ReducingSink();
}
};
}
所有的計算工作都是由 evaluate
函數完成的,如下:
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
assert getOutputShape() == terminalOp.inputShape();
if (linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
linkedOrConsumed = true;
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}
實際上, stream 流計算有兩種模式,一種是串行的方式,一種是并行的方式,首先看串行的方式 evaluateSequential
,如下:
@Override
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
Spliterator<P_IN> spliterator) {
return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}
helper
代表的實際上是除開 max
后的最后一個 stage,即 mapToLong
,可以根據如下函數棧追蹤到運行的函數,如下:
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
/|\
|
helper.wrapAndCopyInto(makeSink(), spliterator);
copyInto
為運行的核心函數,如下:
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
Objects.requireNonNull(wrappedSink);
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}
就是該函數啟動了整個流水線的執行。
Stream 接口并行化
對于無狀態的 stage,是可以開啟并行來處理的,主要是用到了 Java 提供的 Fork/Join
框架。
來看 evaluateParallel
函數,如下:
@Override
public <P_IN> R evaluateParallel(PipelineHelper<T> helper,
Spliterator<P_IN> spliterator) {
return new ReduceTask<>(this, helper, spliterator).invoke().get();
}
ReduceTask
是 ForkJoinTask
的一個子類,invoke()
函數就是開啟計算流程,get()
函數等待計算完成并返回結果。
ForkJoinTask
最重要的就是重寫 compute
函數
@Override
public void compute() {
Spliterator<P_IN> rs = spliterator, ls; // right, left spliterators
long sizeEstimate = rs.estimateSize();
long sizeThreshold = getTargetSize(sizeEstimate);
boolean forkRight = false;
@SuppressWarnings("unchecked") K task = (K) this;
while (sizeEstimate > sizeThreshold && (ls = rs.trySplit()) != null) {
K leftChild, rightChild, taskToFork;
task.leftChild = leftChild = task.makeChild(ls);
task.rightChild = rightChild = task.makeChild(rs);
task.setPendingCount(1);
if (forkRight) {
forkRight = false;
rs = ls;
task = leftChild;
taskToFork = rightChild;
}
else {
forkRight = true;
task = rightChild;
taskToFork = leftChild;
}
taskToFork.fork();
sizeEstimate = rs.estimateSize();
}
task.setLocalResult(task.doLeaf());
task.tryComplete();
}
使用 Spliterator
迭代器產生數據,并且交叉左右節點執行 fork
函數(即分叉計算),在 tryComplete
中執行結果的合并操作,流程圖如下: