Java8學習筆記(1) -- 從函數式接口說起

希望本文能夠成爲Java8 Lambda表達式的快速入門指南。

函數式接口

理解Functional Interface(函數式接口,以下簡稱FI)是學習Java8 Lambda表達式的關鍵所在,所以放在最開始討論。FI的定義其實很簡單:任何接口,如果只包含唯一一個抽象方法,那麼它就是一個FI。爲了讓編譯器幫助我們確保一個接口滿足FI的要求(也就是說有且僅有一個抽象方法),Java8提供了@FunctionalInterface註解。舉個簡單的例子,Runnable接口就是一個FI,下面是它的源代碼:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Lambda語法糖

爲了能夠方便、快捷、幽雅的創建出FI的實例,Java8提供了Lambda表達式這顆語法糖。下面我用一個例子來介紹Lambda語法。假設我們想對一個List<String>按字符串長度進行排序,那麼在Java8之前,可以藉助匿名內部類來實現:

List<String> words = Arrays.asList("apple", "banana", "pear");
words.sort(new Comparator<String>() {

    @Override
    public int compare(String w1, String w2) {
        return Integer.compare(w1.length(), w2.length());
    }

});

上面的匿名內部類簡直可以用醜陋來形容,唯一的一行邏輯被五行垃圾代碼淹沒。根據前面的定義(並查看Java源代碼)可知,Comparator是個FI,所以,可以用Lambda表達式來實現:

words.sort((String w1, String w2) -> {
    return Integer.compare(w1.length(), w2.length());
});
代碼變短了好多!仔細觀察就會發現,Lambda表達式,很像一個匿名的方法,只是圓括號內的參數列表和花括號內的代碼被->分隔開了。垃圾代碼寫的越少,我們就有越多的時間去寫真正的邏輯代碼,不是嗎?是的!圓括號裏的參數類型是可以省略的:

words.sort((w1, w2) -> {
    return Integer.compare(w1.length(), w2.length());
});
如果Lambda表達式的代碼塊只是return後面跟一個表達式,那麼還可以進一步簡化:

words.sort(
    (w1, w2) -> Integer.compare(w1.length(), w2.length())
);
注意,表達式後面是沒有分號的!如果只有一個參數,那麼包圍參數的圓括號可以省略:

words.forEach(word -> {
    System.out.println(word);
});
如果表達式不需要參數呢?好吧,那也必須有圓括號,例如:

Executors.newSingleThreadExecutor().execute(
    () -> {/* do something. */} // Runnable
);

方法引用

有時候Lambda表達式的代碼就只是一個簡單的方法調用而已,遇到這種情況,Lambda表達式還可以進一步簡化爲方法引用(Method References)。一共有四種形式的方法引用,第一種引用靜態方法,例如:

List<Integer> ints = Arrays.asList(1, 2, 3);
ints.sort(Integer::compare);
第二種引用某個特定對象的實例方法,例如前面那個遍歷並打印每一個word的例子可以寫成這樣:

words.forEach(System.out::println);
第三種引用某個類的實例方法,例如:

words.stream().map(word -> word.length()); // lambda
words.stream().map(String::length); // method reference

第四種引用類的構造函數,例如:

// lambda
words.stream().map(word -> {
    return new StringBuilder(word);
});
// constructor reference
words.stream().map(StringBuilder::new);

什麼時候用Lambda表達式

既然Lambda表達式這麼好用,那麼,可以在哪些地方使用呢?如果你真正明白了什麼是FI(很容易),應該立刻就能給出答案:任何可以接受一個FI實例的地方,都可以用Lambda表達式。比如,雖然上面給出的例子都是把Lambda表達式當作方法參數傳遞,但實際上你也可以定義變量:

Runnable task = () -> {
    // do something
};

Comparator<String> cmp = (s1, s2) -> {
    return Integer.compare(s1.length(), s2.length());
};

預定義函數式接口

Java8除了給Runnable,Comparator等接口打上了@FunctionalInterface註解之外,還預定義了一大批新的FI。這些接口都在java.util.function包裏,下面簡單介紹其中的幾個。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Predicate用來判斷一個對象是否滿足某種條件,比如,單詞是否由六個以上字母組成:

words.stream()
    .filter(word -> word.length() > 6)
    .count();

Function表示接收一個參數,併產生一個結果的函數:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
下面的例子將集合裏的每一個整數都乘以2:

ints.stream().map(x -> x * 2);

Consumer表示對單個參數進行的操作,前面例子中的forEach()方法接收的參數就是這種操作:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

對原有API的增強

爲了充分發揮Lambda的威力,Java8對很多老的類庫進行了增強,給它們配備了Lambda武器。比如前面例子中用到的forEach()方法,實際上是添加到Iterable接口中的。而多次出現的stream()方法,則是添加在了Collection接口裏。用過Ruby,Scala,Groovy等語言的Java程序員,可能已經對在這些語言裏很好實現的外部迭代器模式垂涎很久了。雖然Google的Guava可以在一定程度上彌補Java的這種缺陷,但是Java8的Lambda才真正讓Java朝着函數式編程邁進了一大步。

接口的默認方法

細心的讀者可能會發現一個問題,給Iterable和Collection等接口增加方法,豈不是會破壞接口的向後兼容性?是的,爲了保證API的向後兼容性,Java8對接口的語法進行了較大的調整,增加了默認方法(Default Methods)。下面是forEach()方法的實現代碼:

public interface Iterable<T> {
    ...
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    ...
}

接口的靜態方法

除了抽象方法和默認方法,從Java8開始,接口也可以有靜態(static)方法了。有了這個語法,我們就可以把和接口相關的幫助方法(Helper Methods)直接定義在接口裏了。比如Function接口就定義了一個工廠方法indentity():

public interface Function<T, R> {
    ...
    /**
     * Returns a function that always returns its input argument.
     *
     * @param <T> the type of the input and output objects to the function
     * @return a function that always returns its input argument
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
...
}

變量捕獲

內部類一樣,Lambda也可以訪問外部(詞法作用域)變量,規則基本一樣。Java8之前,內部類只能訪問final類型的變量,Java8放寬了這種限制,只要變量實際上不可變(effectively final)就可以。換句話說,如果你給變量加上final關鍵字編譯器也不報錯,那麼去掉final關鍵字後,它就是effectively final的。看下面的例子:

int a = 100;
Runnable x = new Runnable() {

    @Override
    public void run() {
        System.out.println(a);
    }
    
};
在Java8之前,a必須是final的才能被x看到。下面用Lambda表達式重寫上面的例子:

int a = 100;
Runnable x = () -> {
    System.out.println(a);
};

結論

可以看到,爲了支持Lambda表達式,Java8對Java語言做了很大的調整。但Lambda表達式並非只是Java語法糖,而是由編譯器和JVM共同配合來實現的,這一點我會在下一篇文章裏詳細介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章