1. 程式人生 > 實用技巧 >名詞王國裡的新政-解讀Java8之lambda表示式

名詞王國裡的新政-解讀Java8之lambda表示式

前幾天在reddit上看到Java8 M8 Developer Preview版本已經發布了,不免想要嚐鮮一把。Developer Preview版本已經所有Feature都完成了,Java8的特性可以在這裡看到http://openjdk.java.net/projects/jdk8/features,下載地址:http://jdk8.java.net/download.html。Java8最值得期待的就是lambda表示式了,本文就將帶你體驗lambda表示式,並進行比較深入的解析。

下載及配置

Intellij IDEA已經完美支援Java8了。首先開啟Project Structure,在Project裡設定新的JDK路徑,並設定Modules=>Source=>Language Level為8.0即可。

現在我們可以使用Java8編寫程式了!但是當我們開開心心編寫完,享受到高階的lambda表示式後,執行程式,會提示:java: Compilation failed: internal java compiler error!這是因為javacc的版本還不對,在Compiler=>Java Compiler裡將專案對應的javacc版本選為1.8即可。

什麼?你說你用Eclipse?好像目前還沒有穩定版!想嚐鮮的,可以看看這個地址http://stackoverflow.com/questions/13295275/programming-java-8-in-eclipse,大致是先checkout Eclipse JDT的beta java8分支,然後在Eclipse裡執行這個專案,從而啟動一個支援java8的Eclipse…不過應該難不倒作為geek的你吧!

體驗lambda表示式

好了,我們開始體驗Java8的新特性-lambda表示式吧!現在我們的匿名類可以寫成這樣子了:

<!-- lang: java -->
    new Thread(() -> {
        System.out.println("Foo");
    }).start();

而之前的寫法只能是這樣子:

<!-- lang: java -->
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Foo");
        }
    }).start();

這樣一看,我們似乎就是匿名類寫起來簡單了一點啊?而第二種方法,藉助便捷的IDE,好像編寫效率也沒什麼差別?博主開始也是這樣認為,仔細學習之後,才知道其中的奧妙所在!

這裡有一個重要的資訊,就是**()->{}這裡代表一個函式,而非一個物件。**可能這麼說比較抽象,我們還是程式碼說話吧:

<!-- lang: java -->
public class LambdaTest {

    private static void bar(){
        System.out.println("bar");
    }

    public static void main(String[] args) {
        new Thread(LambdaTest::bar).start();
    }

}

看懂了麼?這裡LambdaTest::bar代表一個函式(用C++的同學笑了),而new Thread(Runnable runnable)的引數,可以接受是一個函式作為引數!

是不是覺得很神奇,顛覆了Java思維?在剖析原理以前,博主暫且賣個關子,我們先來講講什麼是lambda表示式。

什麼是lambda表示式

lambda表示式的由來

絮叨幾句,現代程式語言的lamdba表示式都來自1930年代初,阿隆佐·邱奇(Alonzo Church)提出的λ演算(Lambda calculus)理論。λ演算的核心思想就是“萬物皆函式”。一個λ運算元即一個函式,其一般形式是λx.x + 2。一個λ運算元可以作為另一個λ運算元的輸入,從而構建一個高階的函式。λ演算是函數語言程式設計的鼻祖,大名鼎鼎的程式語言Lisp就是基於λ演算而建立。用過Lisp的應該都清楚,它的語法很簡單,但是卻有包容萬物的能力。

可能搞計算機的對邱奇比較陌生,但是提起和邱奇同時代的另外一個人,大家就會覺得如雷貫耳了,那就是阿蘭·圖靈。邱奇成名的時候,圖靈還是個大學生。邱奇和圖靈一起發表了邱奇-圖靈論題,並分別提出了λ演算和圖靈機,加上哥德爾提出的遞迴函式一起,在理論上確定了什麼是可計算性。至於什麼是可計算性,其實博主也說不清楚,但是現代所有計算機程式語言,都可以認為是從三種之一發展而來,並與之等價的。僅此一點,其影響深遠,可想而知。當年教我們《計算理論》的是一個德高望重的教授,人稱宋公,每次講到那個輝煌的年代,總是要停下來,神情專注的感嘆一句:“偉大啊!”想想確實挺偉大,人家圖靈大學時候就奠定了現代計算機的基礎,而我們那會大概還在打DOTA…

附上大神們的照片,大家感受一下:

現代程式語言中的lambda表示式

好了扯遠了,神遊過了那個偉大的時代,我們繼續思考如何編程式碼做需求吧…

現代語言的lambda表示式,大概具備幾個特徵(博主自己歸納的,如有不嚴謹,歡迎指正):

  1. 函式可作為輸入;
  2. 函式可作為輸出;
  3. 函式可作用在函式上,形成高階函式。
  4. 函式支援lambda格式的定義。

其實有了1、2,3也就是順水推舟的事情,而4其實沒有太大的必要性,因為一般語言都有自己的函式定義方式,4僅僅是作為一種補充。當然實現了4的語言,一般都會說:“你看我實現了lambda表示式!”(望向Java8和Python同學)

在Java8中使用lambda表示式

FunctionalInterface

Java中的lambda無法單獨出現,它需要一個介面來盛放。這個介面必須使用@FunctionalInterface作為註解,並且只有一個未實現的方法。等等,什麼叫介面中未實現的方法?難道介面中還可以有已實現的方法?恭喜你,猜對了!Java8的介面也可以寫實現了!是不是覺得Interface和AbstractClass更加傻傻分不清楚了?但是AbstractClass是無法使用@FunctionalInterface註解的,官方的解釋是為了防止AbstractClass的建構函式做一些事情,可能會導致一些呼叫者意料不到的事情發生。

好了,我們來看一點程式碼,Runnable介面現在變成了這個樣子:

<!-- lang: java -->
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

這裡我們可以將任意無引數的lambda表示式賦值給Runnable:

<!-- lang: java -->
    Runnable runnable = () -> {
        System.out.println("Hello lambda!");
    };
    runnable.run();

lambda表示式本質上是一個函式,所以我們還可以用更加神奇的賦值:

<!-- lang: java -->
public class HelloLambda {

    private static void hellolambda() {
        System.out.println("Hello lambda!");
    }

    public static void main(String[] args) {
        Runnable runnable = HelloLambda::hellolambda;
        runnable.run();
    }
}

這裡看到這裡,大家大概明白了,lambda表示式其實只是個幌子,更深層次的含義是:函式在Java裡面可以作為一個實體進行表示了。這就意味著,在Java8裡,函式既可以作為函式的引數,也可以作為函式的返回值,即具有了lambda演算的所有特性。

Function系列API

看到這裡,可能大家會有疑問?什麼樣的函式和什麼樣的lambda表示式屬於同一型別?答案是引數和返回值的型別共同決定函式的型別。例如Runnable的run方法不接受引數,也沒有返回值,那麼Runnable介面則可以用任意沒有引數且沒有返回值的函式來賦值。這樣概念上來說,Runnable表示的含義就從一個物件變成了一個方法。

這一點在Java8中的java.util.function包裡的程式碼得到了驗證。以最具有代表性的Function介面為例:

<!-- lang: java -->
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

}

有了Function,我們可以這樣寫:

<!-- lang: java -->
    Function<Integer,String> convert = String::valueOf;
    String s = convert.apply(1);

這個東東是不是很像Javascript中的函式物件?

可惜的是,這裡的Function算是個半成品,它只能表示一個有單個引數,並有非void返回值的函式。像System.out.println()這種方法,因為返回值為void,是無法賦值為Function的!

怎麼辦?java.util.function包提供了一個不那麼完美的解決方案:多定義幾個FunctionalInterface唄!

於是,在Java8裡有了:

  • Supplier: 沒有引數,只有返回值的函式
  • Consumer: 一個引數,返回值為void的函式
  • BiFunction: 兩個引數,一個返回值的函式
  • BiConsumer: 兩個引數,沒有返回值的函式
  • ...

對於這些個API,我也沒有什麼力氣吐槽了,反正我也想不出更好的方法…大家趁機,多學幾個單詞吧,嗯。

總結:名詞王國的新政

相信很多同學都看過這篇著名的文章:名詞王國裡的死刑。這篇文章吐槽了Java裡,動詞(方法)在Java裡總是要依附於某個名詞(物件/類)存在。

現在動詞在名詞王國終於有了一個身份了。當然這個動詞需要先取得一個名詞的身份(FunctionInterface),然後才能名正言順的倖存下來。好在Oracle國王預先為他們留了一些身份(Function、Consumer、Supplier、BiFunction...),所以大多數動詞都已經找到了自己的位置。System.out.println(String)現在是Consumer<String>了,String.valueOf(Integer)現在是Function<Integer,String>了,Collection.size()現在是Supplier<Integer>了…。要為一些較長引數的方法獲取一個身份,也是挺容易的(定義一個新的FunctionInterface介面)。

我相信這個影響是深遠的。例如下面一段程式碼,可以同一行程式碼將一個List<Integer>轉換成一個List<String>:

<!-- lang: java -->
List<String> strings = intList.stream().map(String::valueOf).collect(Collectors.<String>toList());

當然問題也存在。因為包含了閉包等因素,FunctionInterface的序列化/反序列化會是一個相當複雜的事情。熟悉Java的開發者,也會因為lambda的引入,帶來了一些困惑。俗話說活到老學到老,我倒是不介意這個新功能,你說呢?

參考文獻:

  1. http://blog.sciencenet.cn/blog-414166-628109.html
  2. http://www.global-sci.org/mc/issues/3/no2/freepdf/80s.pdf
  3. http://en.wikipedia.org/wiki/Lambda_calculus
  4. 吸引人的微信軟文編輯方法