歡迎光臨
每天分享高質量文章

利用 Lambda 運算式實現 Java 中的惰性求值

(點選上方公眾號,可快速關註)


來源:ImportNew – yizhe

Java 中惰性求值的潛能,完全被忽視了(在語言層面上,它僅被用來實現 短路求值 )。更先進的語言,如 Scala,區分了傳值呼叫與傳名呼叫,或者引入了 lazy 這樣的關鍵字。

儘管 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);

    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 v2 = () -> 42; // lazy

如果需要花費較長時間才能從函式中獲得結果,上面這個方法會更加實用:

Integer v1 = compute(); //eager

Supplier value = () -> compute(); // lazy

同樣的,這次傳入一個方法作為引數:

private static int computeLazily(Supplier value) {

    // …

}

如果仔細觀察 Java 8 中新增的 API,你會註意到這種樣式使用得特別頻繁。一個最顯著的例子就是 Optional#orElseGet ,Optional#orElse 的惰性求值版本。

如果不使用這種樣式的話,那麼 Optional 就沒什麼用處了… 或許吧。當然,我們不會滿足於 suppliers 。我們可以用同樣的方法復用所有 functional 介面。

執行緒安全和快取

不幸的是,上面這個簡單的方法是有缺陷的:每次呼叫都會觸發一次計算。不僅多執行緒的呼叫有這個缺陷,同一個執行緒連續呼叫多次也有這個缺陷。不過,如果我們清楚這個缺陷,並且合理的使用這個技術,那就沒什麼問題。

使用快取的惰性求值

剛才已經提到,基於 lambda 運算式的方法在一些情況下是有缺陷的,因為傳回值沒有儲存起來。為了修複這個缺陷,我們需要構造一個專用的工具,讓我們叫它 Lazy :

public class Lazy { … }

這個工具需要自身同時儲存 Supplier 和 傳回值 T。

@RequiredArgsConstructor

public class NaiveLazy

 

    private final Supplier supplier;

    private T value;

    public T get() {

 

        if (value == null) {

            value = supplier.get();

         }

        return value;

    }

}

就是這麼簡單。註意上面的程式碼僅僅是一個概念模型,暫時還不是執行緒安全的。

幸運的是,如果想讓它變得執行緒安全,只需要保證不同的執行緒在獲取傳回值的時候不會觸發同樣的計算。這可以簡單的透過雙重檢查鎖定機制來實現(我們不能直接在 get() 方法上加鎖,這會引入不必要的競爭):

@RequiredArgsConstructor

 

public class Lazy {

 

    private final Supplier 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 Lazy map(Function mapper) {

    return new Lazy<>(() -> mapper.apply(this.get()));

}

 

public Lazy flatMap(Function> mapper) {

    return new Lazy<>(() -> mapper.apply(this.get()).get());

}

 

public Lazy> filter(Predicate predicate) {

    return new Lazy<>(() -> Optional.of(get()).filter(predicate));

}

最佳化永無止境。

我們也可以暴露一個方便的工廠方法:

public static Lazy of(Supplier 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 {

    private transient Supplier supplier;

    private volatile T value;

    public Lazy(Supplier 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 Lazy map(Function mapper) {

        return new Lazy<>(() -> mapper.apply(this.get()));

    }

    public Lazy flatMap(Function> mapper) {

        return new Lazy<>(() -> mapper.apply(this.get()).get());

    }

    public Lazy> filter(Predicate predicate) {

        return new Lazy<>(() -> Optional.of(get()).filter(predicate));

    }

    public static Lazy of(Supplier supplier) {

        return new Lazy<>(supplier);

    }

}

以上的程式碼也可以在 GitHub 上找到。

https://github.com/pivovarit/articles/tree/master/java-lazy-initialization

關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂