目前JDK8引進了Lambda專案,包括了Lambda語法、方法參考(Method reference)、Stream API、函數式風格API,以及平行處理等重大特性,如果你要轉進JDK8了,那麼,是不是過去可用的方式,都要改採Lambda的相關特性來實現呢?

面對一個問題會有多種風格的解法時,你是否一定要選擇Lambda呢?

依可讀性考量

從最簡單的角度來看待Lambda語法,就是將之當作匿名類別(Anoynmous class)的語法蜜糖,不用理會類別名稱,也不用管方法名稱,在型態推斷(Type inference)的輔助下,連參數型態與傳回型態都可以省略,撰寫程式時可以少打一些字,對減少程式碼數量是有幫助。

然而,減少程式碼數量不見得有助於可讀性,如果拿掉匿名類別語法上的名稱與型態資訊,反而使得你必須察看API文件才能理解程式碼(特別是在沒有IDE輔助下),那麼你就不應當使用Lambda語法,是否使用方法參考,也是視程式碼展現的可讀性而定。

JDK8中有許多搭配Lambda語法的高階API,這類API抽取了可重用的流程,在名稱上顯露出高階語義,自然地,當你使用它們時,要確認自己(或夥伴)瞭解名稱代表什麼,且不在意內部實作細節,否則的話,反而會在閱讀程式碼時失去了直覺而感到困惑。

舉例來說,Map的forEach語法在迭代鍵值很值得使用,因為相較於使用for迴圈來說,可讀性高且forEach名稱清楚易懂。相對地,如果若原本有段直覺的程式碼:

List<String> itemNames = new ArrayList<>();
for(Order order : orders) {
    for(LineItem lineItem : order.getLineItems()) {
        itemNames.add(lineItem.getName());
    }
}

不用任何說明,從程式碼中可以一眼看出來,執行目的是從每個訂單中收集品項名稱,你可以試著將之改成Lambda版本,兩個版本要撰寫的程式碼數量其實是差不多,思考一下,如果你(或夥伴)對於map、flatMap的語義清不清楚,而這些方法又隱藏了可重用的流程細節,對程式碼的閱讀是否會有幫助?

List<String> itemNames = orders.stream()
       .map(Order::getLineItems)
       .flatMap(lineItems -> lineItems.stream())
       .map(LineItem::getName)
       .collect(toList());

考量管線操作的實質作用

JDK8中搭配Lambda的API多半使用管線(Pipeline)操作,目前作用之一是呈現流暢風格,讓程式碼伴演著內部DSL的角色,除了可讀性的考量之外,可考量實質作用。

舉例來說,Stream API實現了惰性求值(Lazy evaluation),同樣是讀取檔案,Files現在有readAllLines與lines兩個方法可以使用,然而對於以下的情況:

for(String line : readAllLines(get(fileName))) {
    if(line.startsWith(prefix)) {
        matched = line; break;
    }
}

修改為以下的Lambda版本,若檔案內容較大時會比較有效率,因為lines方法不會一次讀入全部檔案,而filter方法也不會對全部的行做測試,第一行內容的讀取與測試發生在findFirst被呼叫之後,在最極端的狀況下,如果檔案中第一行就符合條件,後續的檔案讀取就不會進行:

try(Stream<String> lines = lines(get(fileName))) {
    Optional<String> matched = lines.filter(line -> line.startsWith(prefix))
                                    .findFirst();
}

管線操作中的每個方法呼叫,其實各自隱藏了不少細節,閱讀程式碼時多半沒那麼直覺,然而,如果面對複雜的問題,例如非同步處理的連續組合需求,那麼使用CompletableFuture這類API,來避免我先前專欄〈非同步操作的多種模式〉中談到的回呼地獄(Callback hell),相對來說,如此反而就比較直覺了。

如果你的程式改為Lambda風格,對於可讀性沒有幫助,也沒有管線化操作上的實質意義,像是簡單的陣列迭代,那麼就不必要特別使用Arrays.stream,將陣列包裏為Stream了。

平行處理前的重構手段

使用Lambda相關的API時你會發現,它們幾乎只接受一個Lambda,也就是一次只能指定一件事,當你打算將一個程式區塊轉為Lambda相關作法前,你會因此被迫先對該區塊進行重構,成為數個各自獨立且只產生一個結果的小區塊,而當上一個小區塊的結果會作為下一個小區塊的起始條件時,你就有機會進行管線操作,在《重構:改善既有程式的設計》中就談過,想解構程式中的邏輯泥塊,就是讓它們一次只做一件事。

JDK8提供高階語義的管線化API,目的之一,就是希望你思考處理的過程中,實際上是由哪些小任務組成。

在過去,你可能基於(自我想像的)效能增進考量,在迴圈中做了很多件事,因而讓程式變得複雜,打算重構或許也沒有具體可憑藉的手段,也許,試著從Lambda提供哪些高階API來思考會是個可行方向,畢竟為了套用API,被迫重構的成份居多,然而換取而來的效果,就是API會在可能的情況下實現惰性、平行處理能力。

就Lambda語法本身,以及JDK8的高階語義API,許多概念都可追溯至函數式程式設計,不過,這並不是鼓勵Java開發者撰寫純函數程式,而是要讓開發者在撰寫平行處理程式碼時能更為簡化,在最簡單的情況下,可以將stream方法換為parallelStream,就能得到平行處理的可能性。

然而,天下沒有白吃的午餐,在這之前,開發者要懂得讓程式碼一次做一件事,將複雜的程式區塊切割成許多小任務,另外,還要留意程式執行順序,並且避免在每個小任務執行時,對Stream來源資料進行干擾。

多一種工具、多一個思考角度

從其他語言來看Lambda的東西都不是新的,就算只從Java的領域來看Lambda的東西也不是新的,像是JDK8中許多API,在guava-libraries中都有對應的類似API,只不過,在缺少Lambda語法之下,多少影響了使用的意願。

guava-libraries在一篇〈FunctionalExplained〉也談到,除非使用這些API進行函數式風格設計時,對可讀性有所幫助,或者是取得了惰性處理上的一些益處,不然就算使用了guava-libraries,命令式仍應是JDK7與先前版本的風格選擇,為此,對於同一件事的解決,guava-libraries在不少API中,都提供了兩種風格的選擇。

實際上,這類考量在JDK8之後依然是可行的,解決問題不僅一種方式,就算是簡單的迭代,也有迴圈或forEach兩種風格可以選擇,在Lambda語法的直接支援下,選擇哪種工具解決問題都很方便,當手邊多了一種工具,實際上你是多了一個思考角度,拒絕使用,或一味認為用上最新的東西就是最好,都只是在放棄多一個思考角度的機會。

專欄作者

熱門新聞

Advertisement