程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java中的惰性計算簡介

Java中的惰性計算簡介

編輯:關於JAVA

惰性計算(盡可能延遲表達式求值)是許多函數式編程語言的特性。惰性集合在需要時提供其元素,無需預先計算它們 ,這帶來了一些好處。首先,您可以將耗時的計算推遲到絕對需要的時候。其次,您可以創造無限個集合,只要它們繼續收 到請求,就會繼續提供元素。第三,map 和 filter 等函數的惰性使用讓您能夠得到更高效的代碼。Java 並沒有為惰性提 供原生支持,但一些框架和後繼語言支持這種惰性,我會在本期和下期文章中探討它們。

假定使用此偽代碼片段來 打印列表的長度:

print length([2+1, 3*2, 1/0, 5-4])

如果您嘗試執行此代碼,結果會因為代碼的編程語言類型的不同而有所不同:嚴格或不嚴格(也被稱為惰性)。在嚴格 的編程語言中,執行(或編譯)此代碼產生一個 DivByZero 異常,原因是列表的第三個元素。在不嚴格的語言中,其結果 是 4,它准確地報告了列表中的項目數。畢竟,我調用的方法是 length(),而不是 lengthAndThrowExceptionWhenDivByZero()!Haskell 是為數不多的仍在使用的不嚴格語言。可惜的是,Java 不支持不嚴 格的計算,但您仍然可以在 Java 中使用惰性的概念。

在 Java 中的惰性迭代器

Java 缺乏對惰性集合的原 生支持,但這並不意味著您不能使用 Iterator 模擬一個惰性集合。在本系列的前幾篇文章中,我使用了一個簡單的素數算 法來說明函數式概念。我會在 上期文章 中介紹的優化類的基礎上展開本文的討論,同時提供清單 1 中展示的增強:

清單 1. 確定素數的簡單算法

import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;
    
public class Prime {
    
    public static boolean isFactor(int potential, int number) {
        return number % potential == 0;
    }
    
    public static Set<Integer> getFactors(int number) {
        Set<Integer> factors = new HashSet<Integer>();
        factors.add(1);
        factors.add(number);
        for (int i = 2; i < sqrt(number) + 1; i++)
            if (isFactor(i, number)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }
    
    public static int sumFactors(int number) {
        int sum = 0;
        for (int i : getFactors(number))
            sum += i;
        return sum;
    }
    
    public static boolean isPrime(int number) {
        return number == 2 || sumFactors(number) == number + 1;
    }
    
    public static Integer nextPrimeFrom(int lastPrime) {
        lastPrime++;
        while (! isPrime(lastPrime)) lastPrime++;
        return lastPrime;
    }
}

前面的一期文章 詳細討論了這個類是如何確定某個整數是否是素數的細節。在 清單 1 中,我添加了 nextPrimeFrom() 方法,根據輸入的參數生成下一個素數。該方法在本文即將出現的示例中發揮了重要的作用。

一 般情況下,開發人員認為迭代器會使用集合作為後備存儲,但是支持 Iterator 接口的任何集合都符合這個條件。因此,我 可以創建一個素數的無限迭代器,如清單 2 所示:

清單 2. 創建一個惰性迭代器

public class 

PrimeIterator implements Iterator<Integer> {
    private int lastPrime = 1;
    
    public boolean hasNext() {
        return true;
    }
    
    public Integer next() {
        return lastPrime = Prime.nextPrimeFrom(lastPrime);
    }
    
    public void remove() {
       throw new RuntimeException("Can't change the fundamental nature of the universe!");
    }
}

在 清單 2 中,hasNext() 方法始終返回 true,因為就我們目前所掌握的知識,素數的數量是無限的。remove () 方法在此處不適用,所以在意外調用情況下,會拋出一個異常。沉穩的做法是使用 next() 方法,它用一行代碼處理兩 件事。第一,它調用我在 清單 1 中添加的 nextPrimeFrom() 方法,根據上一個素數生成下一個素數。第二,它利用了 Java 在單個語句中完成賦值與返回結果的能力,更新內部的 lastPrime 字段。我在清單 3 中執行惰性迭代器:

清 單 3. 測試惰性迭代器

public class PrimeTest {
    private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
                add(2);  add(3);  add(5);  add(7);  add(11);  add(13);
                add(17); add(19); add(23); add(29); add(31);  add(37);
                add(41); add(43); add(47);
            }};
    
    @Test
    public void prime_iterator() {
        Iterator<Integer> it = new PrimeIterator();
        for (int i : PRIMES_BELOW_50) {
            assertTrue(i == it.next());
        }
    }
}

在 清單 3中,我創建了一個 PrimeIterator,並驗證它會報告前 50 個素數。雖然這不是迭代器的典型用法, 但它模仿一些惰性集合的有用行為。  

使用 LazyList

Jakarta Commons 包括一個 LazyList 類,它結 合使用了裝飾設計模式和工廠。如果要使用 Commons LazyList,則必須包裝一個現有列表,使其具有惰性,並為新值創建 一個工廠。請考慮使用清單 4 中的 LazyList:

清單 4. 測試 Commons LazyList  

public class 

PrimeTest {
    private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
                add(2);  add(3);  add(5);  add(7);  add(11);  add(13);
                add(17); add(19); add(23); add(29); add(31);  add(37);
                add(41); add(43); add(47);
            }};
    
    @Test
    public void prime_factory() {
        List<Integer> primes = new ArrayList<Integer>();
        List<Integer> lazyPrimes = LazyList.decorate(primes, new PrimeFactory());
        for (int i = 0; i < PRIMES_BELOW_50.size(); i++)
            assertEquals(PRIMES_BELOW_50.get(i), lazyPrimes.get(i));
    }
}

在 清單 4 中,我創建一個新的空白 ArrayList 並將它包裝在 Commons LazyList.decorate() 方法中,還有一 個 PrimeFactory 用於生成新值。Commons LazyList 使用列表中已存在的值,不論該值是什麼,當對一個尚未賦值的索引 調用 get() 方法時,LazyList 會使用工廠(在本例中為 PrimeFactory())生成和填充值。PrimeFactory 出現在清單 5 中:

清單 5. LazyList 使用的 PrimeFactory

public class PrimeFactory implements Factory {
    private int index = 0;
    
    @Override
    public Object create() {
        return Prime.indexedPrime(index++);
    }
}

所有惰性列表都需要使用某種方法來生成後續的值。在 清單 2 中,我使用了 next() 方法和 Prime 的 nextPrimeFrom() 方法的組合。對於 清單 4 中的 Commons LazyList,我使用了 PrimeFactory 實例。

Commons LazyList 實現有一個特點,就是在請求新值時,沒有信息傳遞給工廠方法。根據設計,它甚至沒有傳遞所請求元素的索引 ,強制在 PrimeFactory 類上維護當前狀態。這產生了對返回列表的不必要的依賴(因為它必須初始化為空,以便讓索引和 PrimeFactory 的內部狀態保持同步)。Commons LazyList 是最好的基礎實現,但還有更好的開源替代方案,如 Totally Lazy。

Totally Lazy

Totally Lazy 是一個框架,它將一流的惰性添加到 Java。在 前面的一期文章 中,我 介紹過 Totally Lazy,但介紹得並不詳細。該框架的目標之一是使用靜態導入集合來創建一個高度可讀的 Java 代碼。清 單 6 中簡單的素數查找程序在編寫時充分利用了這個 Totally Lazy 特性:

清單 6. Totally Lazy,充分利用靜態 導入

import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;
    
import static com.googlecode.totallylazy.Predicates.is;
import static com.googlecode.totallylazy.numbers.Numbers.equalTo;
import static com.googlecode.totallylazy.numbers.Numbers.increment;
import static com.googlecode.totallylazy.numbers.Numbers.range;
import static com.googlecode.totallylazy.numbers.Numbers.remainder;
import static com.googlecode.totallylazy.numbers.Numbers.sum;
import static com.googlecode.totallylazy.numbers.Numbers.zero;
import static com.googlecode.totallylazy.predicates.WherePredicate.where;
    
public class Prime {
    public static Predicate<Number> isFactor(Number n) {
        return where(remainder(n), is(zero));
    }
    
    public static Sequence<Number> factors(Number n){
        return range(1, n).filter(isFactor(n));
    }
    
    public static Number sumFactors(Number n){
        return factors(n).reduce(sum);
    }
    
    public static boolean isPrime(Number n){
        return equalTo(increment(n), sumFactors(n));
    }
}

在 清單 6 中,在完成靜態導入後,代碼是非典型的 Java,但有很強的可讀性。Totally Lazy 的部分靈感來自 JUnit 的 Hamcrest 測試擴展的接口,它還使用了 Hamcrest 的一些類。isFactor() 方法變成了對 where() 的調用,結合 使用了 Totally Lazy 的 remainder() 與 Hamcrest is() 方法。同樣,factors() 方法變成了針對 range() 對象調用 filter(),我使用現已熟悉的 reduce() 方法來確定總和。最後,isPrime() 方法使用 Hamcrest 的 equalTo() 方法來確 定因數的總和是否等於遞增的數字。

細心的讀者會注意到,清單 6 中的實現的確優化了我在前面的一期文章 中所 提及的實現,使用了更高效的算法來確定因數。優化後的版本如清單 7 所示:

清單 7. 優化的素數查找程序的 Totally Lazy 實現

public class PrimeFast {
    public static Predicate<Number> isFactor(Number n) {
        return where(remainder(n), is(zero));
    }
    
    public static Sequence<Number> getFactors(final Number n){
        Sequence<Number> lowerRange = range(1, squareRoot(n)).filter(isFactor(n));
        return lowerRange.join(lowerRange.map(centeride().apply(n)));
    }
    
    public static Sequence<Number> factors(final Number n) {
        return getFactors(n).memorise();
    }
    
    public static Number sumFactors(Number n){
        return factors(n).reduce(sum);
    }
    
    public static boolean isPrime(Number n){
        return equalTo(increment(n), sumFactors(n));
    }
}

清單 7 中有兩個主要變化。首先,我改進了 getFactors() 算法,以獲得平方根下的因數,然後生成平方根之 上的對稱因數。在 Totally Lazy 中,即使像 centeride() 這樣的操作也可以使用連貫接口樣式來表達。第二個變化涉及內存 ,它會自動緩存使用相同參數的函數調用,我已經修改了 sumFactors() 方法,以便使用 getFactors() 方法,它是內存化 的 getFactors() 方法。Totally Lazy 將內存實現為框架的一部分,因此,實現此優化並不需要更多的代碼,但框架的作 者將其拼寫為 memorise(),而不是更傳統的(如在 Groovy 中)memoize()。

像 Totally Lazy 這個名稱所聲明的 那樣,Totally Lazy 試圖在整個框架中盡可能使用惰性。事實上,Totally Lazy 框架本身就包含了一個 primes() 生成程 序,它使用框架的構建塊實現素數的無限序列。請考慮 Numbers 類的節選代碼,如清單 8 所示:

清單 8. 實現無 限素數的 Totally Lazy 節選代碼

public static Function1<Number, Number> nextPrime = new 

Function1<Number, Number>() {
       @Override
       public Number call(Number number) throws Exception {
              return nextPrime(number);
      }
};
    
public static Computation<Number> primes = computation(2, computation(3, nextPrime));
    
public static Sequence<Number> primes() {
       return primes;
}
     
public static LogicalPredicate<Number> prime = new LogicalPredicate<Number>() {
    public final boolean matches(final Number candidate) {
        return isPrime(candidate);
    }
    
};
    
public static Number nextPrime(Number number) {
    return iterate(add(2), number).filter(prime).second();
}

nextPrime() 方法創建了一個新的 Function1,它是一個偽高階函數的 Totally Lazy 實現,該方法旨在接受單 一 Number 參數,並產生一個 Number 結果。在本例中,它返回 nextPrime() 方法的結果。primes 變量已創建,用於存儲 素數的狀態,使用 2(第一個素數)作為種子值執行計算,並使用一個新的計算來產生下一個素數。這是惰性實現中的典型 模式:保存下一個元素,加上用於產生隨後的值的方法。prime() 方法僅僅是一個包裝程序,圍繞之前執行的 prime 計算 。

為了確定 清單 8 中的 nextPrime(),Totally Lazy 創建了一個新的 LogicalPredicate,以封裝已確定的素數 ,然後創建 nextPrime() 方法,它在 Totally Lazy 中使用良好的接口來確定下一個素數。

在 Java 中使用低層的 靜態導入,以促進可讀代碼的使用,Totally Lazy 在這方面表現得很出色。許多開發人員認為 Java 對於內部的域特定語 言是較差的主機,但 Totally Lazy 消除了這種態度。它積極地采用惰性,延緩所有可能的操作。

結束語

在 這一期文章中,我探索了惰性計算,首先在 Java 中使用迭代器創建一個模擬惰性集合,然後使用了來自 Jakarta Commons Collections 的基本 LazyList 類。最後,我利用了 Totally Lazy 來實現示例代碼,在內部與素數的惰性無限集合中,使 用惰性集合來確定素數。Totally Lazy 也說明了良好接口表示,並使用靜態導入來提高代碼的可讀性。

在下一期文 章中,我將繼續探索惰性計算,並將重心轉移到 Groovy、Scala 和 Clojure。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved