利用 Lambda 表達式實現 Java 中的惰性求值
盡管 Java 8 通過延遲隊列的實現(java.util.stream.Stream)在惰性求值的方面有些改進,但是我們會先跳過 Stream,而把重點放在如何使用 lambda 表達式實現一個輕量級的惰性求值。
基於 lambda 的惰性求值
Scala
當我們想對 Scala 中的方法參數進行惰性求值時,我們用“傳名調用”來實現。
讓我們創建一個簡單的 foo 方法,它接受一個 String 示例,然後返回這個 String:
def foo(b: String): String = b
一切都是馬上返回的,跟 Java 中的一樣。如果我們想讓 b 的計算延遲,可以使用傳名調用的語法,只要在 b 的類型聲明上加兩個符號,來看:
def foo(b: => String): String = b
如果用 javap 反編譯上面生成的 *.class 文件,可以看到:
Compiled from "LazyFoo.scala"
public final class LazyFoo {
public static java.lang.String foo(scala.Function0<java.lang.String>); Code: 0: getstatic #17 // Field LazyFoo.MODULE:LLazyFoo$; 3: aload_0 4: invokevirtual #19 // Method LazyFoo$.foo:(Lscala/Function0;)Ljava/lang/String; 7: areturn
}
看起來傳給這個函數的參數不再是一個 String 了,而是變成了一個 Function0,這使得對這個表達式進行延遲計算變得可能 —— 只要我們不去調用他,計算就不會被觸發。Scala 中的惰性求值就是這麽簡單。
使用 Java
現在,如果我們需要延遲觸發一個返回 T 的計算,我們可以復用上面的思路,將計算包裝為一個返回 Supplier 實例的 Java Function0 :
Integer v1 = 42; // eager
Supplier<Integer> v2 = () -> 42; // lazy
如果需要花費較長時間才能從函數中獲得結果,上面這個方法會更加實用:
Integer v1 = compute(); //eager
Supplier<Integer> value = () -> compute(); // lazy
同樣的,這次傳入一個方法作為參數:
private static int computeLazily(Supplier<Integer> value) {
// ...
}
如果仔細觀察 Java 8 中新增的 API,你會註意到這種模式使用得特別頻繁。一個最顯著的例子就是 Optional#orElseGet ,Optional#orElse 的惰性求值版本。
如果不使用這種模式的話,那麽 Optional 就沒什麽用處了… 或許吧。當然,我們不會滿足於 suppliers 。我們可以用同樣的方法復用所有 functional 接口。
線程安全和緩存
不幸的是,上面這個簡單的方法是有缺陷的:每次調用都會觸發一次計算。不僅多線程的調用有這個缺陷,同一個線程連續調用多次也有這個缺陷。不過,如果我們清楚這個缺陷,並且合理的使用這個技術,那就沒什麽問題。
使用緩存的惰性求值
剛才已經提到,基於 lambda 表達式的方法在一些情況下是有缺陷的,因為返回值沒有保存起來。為了修復這個缺陷,我們需要構造一個專用的工具,讓我們叫它 Lazy :
public class Lazy<T> { ... }
這個工具需要自身同時保存 Supplier 和 返回值 T。
@RequiredArgsConstructor
public class NaiveLazy<T> {
private final Supplier<T> supplier;
private T value;
public T get() {
if (value == null) {
value = supplier.get();
}
return value;
}
}
就是這麽簡單。註意上面的代碼僅僅是一個概念模型,暫時還不是線程安全的。
幸運的是,如果想讓它變得線程安全,只需要保證不同的線程在獲取返回值的時候不會觸發同樣的計算。這可以簡單的通過雙重檢查鎖定機制來實現(我們不能直接在 get() 方法上加鎖,這會引入不必要的競爭):
@RequiredArgsConstructor
public class Lazy<T> {
private final Supplier<T> supplier;
private volatile T value;
public T get() {
if (value == null) {
synchronized (this) {
if (value == null) {
value = supplier.get();
}
}
}
return value;
}
}
現在,我們有了一個完整的 Java 惰性求值的函數化實現。由於它不是在語言的層面實現的,需要付出創建一個新對象的代價。
更深入的討論
當然,我們不會就此打住,我們可以進一步的優化這個工具。比如,通過引入一個惰性的 filter()/flatMap()/map() 方法,可以讓它使用起來更加流暢,並且組合性更強:
public <R> Lazy<R> map(Function<T, R> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()));
}
public <R> Lazy<R> flatMap(Function<T, Lazy<R>> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()).get());
}
public Lazy<Optional<T>> filter(Predicate<T> predicate) {
return new Lazy<>(() -> Optional.of(get()).filter(predicate));
}
優化永無止境。
我們也可以暴露一個方便的工廠方法:
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
實際使用上:
Lazy.of(() -> compute(42))
.map(s -> compute(13))
.flatMap(s -> lazyCompute(15))
.filter(v -> v > 0);
你可以看到,只要作為調用鏈底層的 #get 方法沒有被調用,那麽什麽計算也不會觸發。
Null 的處理
某些情況下,null 會被當做有意義的值。不過它與我們的實現有沖突 —— 一個有意義的 null 值被當做一個未初始化的值,這不太合適。
解決方法也很簡單,直接把這種可能的結果包裝到一個 Optional 實例裏返回。
除此之外,明確禁止 null 作為返回值也是一個好辦法,比如:
value = Objects.requireNonNull(supplier.get());
回收不再使用的 Supplier
有些讀者可能已經註意到了,結果計算完畢之後,supplier 就不再使用了,但是它仍然占據一些資源。
解決辦法就是把 Supplier 標記為非 final 的,一旦結果計算完畢,就把它置為 null。
完整的例子
public class Lazy<T> {
private transient Supplier<T> supplier;
private volatile T value;
public Lazy(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
public T get() {
if (value == null) {
synchronized (this) {
if (value == null) {
value = Objects.requireNonNull(supplier.get());
supplier = null;
}
}
}
return value;
}
public <R> Lazy<R> map(Function<T, R> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()));
}
public <R> Lazy<R> flatMap(Function<T, Lazy<R>> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()).get());
}
public Lazy<Optional<T>> filter(Predicate<T> predicate) {
return new Lazy<>(() -> Optional.of(get()).filter(predicate));
}
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
}
利用 Lambda 表達式實現 Java 中的惰性求值