about 4 years ago

Java 8終於在2014的3月18日正式釋出了,不過自從用Objective C開發iOS App後,我已經有好一陣子沒碰Java,期間曾經有短暫寫一點點,但卻沒有時間去用beta版的Java 8,直到最近才又開始玩一下。Java 8最亮眼的特色之一應該就是所謂的Lambda表示法,Lambda表示法幾乎內建在很多語言中,而我用最多的應該在JavaScript和Objective C (code block)中了。但老實說,我對於Lambda其實不怎麼有愛,JavaScript版的寫法我覺得還好,但Objective C的in place code block我看了覺得好亂,後來大多數的情況下,我都用method回傳code block的方式在使用。

既然,Java 8開始支援Lambda表示法,那程式的撰寫上會變成怎樣呢?首先,以排序為例,先回到沒有Lambda的Java世界,要排序一個List,要先寫一個客製的Comparator (Code List 1的IntegerComparator),然後再建立一個comparator物件作為List.sort()函式的參數(如Code List 2中的sortWithComparator函式),就這點,很多人覺得Java很沒有生產力(Productivity),不過我個人是還蠻喜歡這種寫法,我喜歡寫很多小而簡單的class,因為用JUnit測試這些class也很容易(小class邏輯相對簡單易測),另一個好處是越是小的class越是容易重複利用。

如果不想另外寫一個class,在沒有Lambda的Java世界中其實還有一種寫法,也就是匿名class (anonymous class),如Code List 2中的sortWithAnonymousClass函式就是這種寫法,這種寫法讓我想起早期剛開始學Java時用JBuilder這類的IDE拖拉UI,然後在按鈕上點2下,IDE會自動產生一段程式碼,然後引導你到某個位置開始寫程式(嗯...Visual Basic 6好像也是這樣),IDE產生出來的程式大多是這類的匿名class。在學了比較正式的物件導向設計後,我幾乎不再用這種寫法,只有偶而在比較沒有影響整體設計的情況下,會偷懶使用一下。不過這種寫法還是有人覺得沒生產力。

Code List 1 - IntegerComparator
package java8.lambdas;

import java.util.Comparator;

public class IntegerComparator implements Comparator<Integer> {

    private boolean _ascending;

    public IntegerComparator() {
        this(true);
    }

    public IntegerComparator(boolean ascending) {
        _ascending = ascending;
    }

    @Override
    public int compare(Integer number1, Integer number2) {
        return _ascending? number1.compareTo(number2) : number2.compareTo(number1);
    }
}

Lambda出現後,程式變成sortWithLambdaExpression函式所示那樣,確實行數減少不少,也有人認為可讀性提高不少,認為可讀性提高的原因是,程式碼就在那裡,不像過去還需要跳到另外一個class才能看到實作。關於可讀性這一點,我覺得只有在Lambda內的程式邏輯很簡單才成立,如果邏輯很複雜,把兩個不同邏輯的程式碼放在一起,不僅長度變長,可讀性反而降低,與其這樣還不如抽出來成為一個class並給予一個有意義的class名稱。至於語法用(arguments) -> {implementation}表示,可能是Objective C的code block看多了,有比剛開始第一次看到Java Lambda表示法稍微習慣多了。

Java 8除了這種匿名的Lambda表示法,其實也是支援將既有函式當成Lambda使用的用法,例如Code List 2中的sortWithMethodReferencesortWithStaticMethodReference。不知道大家覺得sortWithLambdaExpressionsortWithMethodReference兩相比較下,哪個可讀性較高呢?我個人是覺得sortWithMethodReference比較高,因為從函式的名稱就可以知道是以升冪的方式排序。我個人在寫JavaScript時也是比較喜歡這種寫法。

Code List 2 - LambdaExample
package java8.lambdas;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class LambdaExample {

    public static void main(String[] arguments) {
        if(arguments.length == 0) {
            System.err.print("Nothing to sort. Abort!");
        }
        else
        {
            LambdaExample lambdas = new LambdaExample();
            List<Integer> numbers = new ArrayList<Integer>();
            for(String argument : arguments) {
                numbers.add(Integer.parseInt(argument));
            }
            lambdas.sortWithComparator(new ArrayList(numbers));
            lambdas.sortWithAnonymousClass(new ArrayList(numbers));
            lambdas.sortWithLambdaExpression(new ArrayList(numbers));
            lambdas.sortWithMethodReference(new ArrayList(numbers));
            lambdas.sortWithStaticMethodReference(new ArrayList(numbers));
        }
    }

    public void sortWithComparator(List<Integer> numbers) {
        IntegerComparator ascendingComparator = new IntegerComparator();
        numbers.sort(ascendingComparator);
        print("sort with comparator", numbers);
    }

    public void sortWithAnonymousClass(List<Integer> numbers) {
        numbers.sort(new Comparator<Integer>() {

            @Override
            public int compare(Integer number1, Integer number2) {
                return number1.compareTo(number2);
            }
        });
        print("sort with inner class", numbers);
    }

    public void sortWithLambdaExpression(List<Integer> numbers) {
        numbers.sort((number1, number2) -> number1.compareTo(number2));
        print("sort with Lambda", numbers);
    }

    public void sortWithMethodReference(List<Integer> numbers) {
        numbers.sort(this::compareAscending);
        print("sort with method reference", numbers);
    }

    public void sortWithStaticMethodReference(List<Integer> numbers) {
        numbers.sort(Integer::compare);
        print("sort with static method reference", numbers);
    }

    public int compareAscending(Integer number1, Integer number2) {
        return number1.compareTo(number2);
    }

    private void print(String approach, List<Integer> numbers) {
        System.out.println(approach);
        for(Integer number : numbers) {
            System.out.print(String.format("%d ", number));
        }
        System.out.println();
    }
}

有人可能會提到Closure才是最主要的精神,關於這點,我想留到下一篇在討論,這篇先討論Lambda對於可讀性和生產力的影響。就可讀性來說,剛剛已經提過了,如果是簡單的邏輯用Lambda包起來,確實有提高一部分的可讀性,但如果邏輯很複雜,混雜在一起我個人是覺得反而降低可讀性。就生產力來說,Lambda某種程度上可以少寫一些程式碼(組成類別的宣告等最低成本),和剛剛相同,如果邏輯簡單,例如Code List 1中compare函式只有一行,那為了這一行所付出的成本是很高的,但如果邏輯複雜,少寫最低成本所產生的生產力提升效益就有限了。

而且生產力不能只看少寫多少行程式,以長遠來看,好測試的程式碼反而能帶來更高的生產力,這一點也是我最近開發iOS App時大量使用method回傳code block的原因,我認為提高可測性所產生的生產力提升效益比直接少寫程式碼來的高很多,畢竟維護程式碼的時間遠比撰寫程式碼的時間來的多很多。以Code List 3為例,doSomething可能是一個整合的函式,其中夾雜了一個Lambda表示法的邏輯在裡面,為了測這段Lambda表示法的邏輯,必須透過測試doSomething來完成,但如果是一個單獨的class如IntegerComparator這樣,可以不用透過測doSomething來完成,而是直測IntegerComparator。或是用method reference的方式也很好測。

Code List 3 - Testability
public void doSomething() {
    List<Integer> numbers = complexPreparation();
    numbers.sort((number1, number2) -> number1.compareTo(number2));
    complexPostProcess(numbers);
}

有人可能覺得Code List 3的例子不太好,確實Code List 3要提高可測性的方法還有很多,不過,我認為匿名Lambda表示法的可測性是很低的。所以如果真的要使用Java 8的Lambda表示法,我應該會傾向使用method reference的方式,除了可測性外,另一個原因是, method reference可以根據OO的準則,例如single responsibility principle,將合適的函式放在合適的class中,然後用method reference的方式直接引用作為Lambda來使用。如此一來,既可提高可讀性(函式名稱本身可以提高可讀性)、可維護性(好測試,好的OO設計),也提高了可重複利用的程度(匿名Lambda無法重複利用,但method reference可以)。

Quick Glance of Java 8 - Lambda →