和Java 8 的Lambdas的第一次親密接觸

jopen 11年前發布 | 24K 次閱讀 Java 8

和Java 8 的Lambdas的第一次親密接觸

        英文原文:Java 8: The First Taste of Lambdas

        Lambda 工程是即將 到來的 Java8 的一大主題,可能也是程序員們最期待已久的東西。隨著 Java lambdas 的到來,還有一個有趣的東西被附帶的加進了 Java 語言——defender (守衛者)方法。在這篇文章里,我的目的是要看看面紗后的東西——看看在運行時環境里 lambdas 是表現的,在方法的調度過程中涉及到哪些字節碼指令。

        盡管 Java 8 還沒有正式發布,我們仍然可以下載各種平臺上的早期預覽版,在其上做簡單的嘗試。

        你也想試試 lambdas,是嗎?

        如果你熟悉其它的還有 lambda 表達式的編程語言,比如 Groovy 或 Ruby,當第一眼看到 Java 里的 lambda 時,你也許會吃驚于它的不簡單。在 Java 里,lambda 表達式是“SAM”(Single Abstract Method)——一個含有一個抽象方法的接口(是的,現在接口里可以含有一個非抽象的方法,defender 守衛方法)。

        舉個例子,大家熟知的Runnable接口就可以完美的被當作一個 SAM 類型:

Runnable r = () -> System.out.println ("hello lambda!");

        這同樣也適用于 Comparable 接口:

Comparator cmp = (x, y) -> (x < y) ? -1 : ((x > y) ? 1 : );

        寫成下面的樣子也是一樣的:

Comparator cmp = (x, y) -> {
  return (x < y) ? -1 : ((x > y) ? 1 : );
};

        從中可以看出,單行的 lambda 表達式似乎是隱含了一個return語句。

        那么,如何寫一個能接受 lambda 表達式作為參數的方法呢?這樣,你需要先把這個參數聲明成函數式的接口,然后把 lambda 傳入:

interface Action {
   void run (String param);
}public void execute (Action action){
   action.run ("Hello!");
}

        一旦有了一個能將函數式接口作為參數的方法,我們就可以像下面這樣調用它:

execute ((String s) -> System.out.println (s));

        還可以更簡潔,這個表達式可以被替換成對一個方法的引用,因為它只是單個方法,而且它們的參數是相同的:

execute (System.out::println);

        然而,如果參數上有任何其它形式的變化,我們就不能直接引用方法,必須寫全 lambda 表達式:

execute ((String s) -> System.out.println ("*" + s + "*"));

        我覺得這種語法還是相當漂亮的,現在,Java 語言里有了一個非常優雅的 lambdas 解決方案,盡管 Java 里并不存在函數式類型。

        JDK 8 里的函數式接口

        我們已經知道,lambda 在運行時的表現形式是一個函數式的接口(或“SAM 類型”)——只有一個抽象方法的接口。盡管 JDK 里已經有了不少這樣的接口,例如Runnable 和 Comparable ,它們符合這種標準,但很顯然,對于一個新 API 的進化來說,這是不夠的。我們不可能所有地方都用 Runnables 接口。

        在 JDK 8 里有個新包,java.util.function,里面包含了很多函數式接口,都是提供在新 API 里使用的。我不想把它們全列出來——你們自己可以去看一下,學習一下這個新包 

        但看起來這個新包在不斷的變化,經常性的一些新接口會出現而另一些會消失。例如,以前曾有過 java.util.function.Block 這個類,最新的版本中卻沒有它,我寫這篇博客時使用的版本是:

anton$ java -version
openjdk version "1.8.0-ea" OpenJDK Runtime Environment (build 1.8.-ea-b75)
OpenJDK 64-Bit Server VM (build 25.0-b15, mixed mode)

        我研究發現,它現在被 Consumer 接口替代,collection 包里的所有新方法都將使用它。例如,Collection接口里定義了forEach方法,如下:

public default void forEach (Consumer consumer) {
  for (T t : this) {
    consumer.accept (t);
  }
}

        Consumer接口里一個有趣地方是,它實際上定義了一個抽象方法——accept (T t)和一個 defender 方法——Consumer chain (Consumer consumer)。這就是說你可以鏈式調用這個接口。我不確定如何使用,因為我在 JDK 包里沒有找到chain (..)的使用方法說明。

        我還發現所有的接口都使用了@FunctionalInterface 運行時注注解注釋。這個注釋不僅僅是個說明,它還被 javac 使用來驗證這個接口是否真是一個函數式接口,是否至少有一個抽象方法在里面。

        所以,如果我們來編譯下面的這段代碼

@FunctionalInterfaceinterface Action {
  void run (String param);
  void stop (String param);
}

        編譯器會告訴我們:

java: Unexpected @FunctionalInterface annotation
  Action is not a functional interface     multiple non-overriding abstract methods found in interface Action

        而下面的就能編譯通過:

@FunctionalInterfaceinterface Action {
  void run (String param);
  default void stop (String param){}
}

        反編譯 lambdas

        我對語法語言特征其實并不是很好奇,我更好奇的是這些特征在運行時的表現形式,這就是為什么我像往常一樣,拿起我喜愛的javap工具,開始查看 lambdas 里的這些類的字節碼

        目前(在 Java 7 之前),如果你想在 Java 里模擬 lambdas,你需要定義一個匿名的內部類。它在編譯后會產生一個具體的 class。如果你在一段代碼里定義了多個這樣的類,你會發現這些類后面會跟著一些數字。那 lambdas 也會這樣嗎?

        看看下面的這段代碼:

public class Main {

  @FunctionalInterface
  interface Action {
    Object run (String s);
  }

  public void action (Action action){
    action.run ("Hello!");
  }

  public static void main (String[] args) {
    new Main () .action ((String s) -> System.out.print ("*" + s + "*"));
  }

}

        編譯產生了兩個類文件:Main.class 和 Main$Action.class,沒有匿名類實現里那樣的序號化的類。那么,在Main.class里應該會有一些東西來代表我在 main 方法里定義的 lambdas 表達式的實現。

$ javap -p Main 

Warning: Binary file Main contains com.zt.Main
Compiled from "Main.java" public class com.zt.Main {
  public com.zt.Main ();
  public void action (com.zt.Main$Action);
  public static void main (java.lang.String[]);
  private static java.lang.Object lambda$(java.lang.String);
}

        啊哈!編譯類了產生了lambda$0方法!使用-c -v指示符會讓我們看到真正的字節碼,以及常量池的定義。

        main 方法里顯示,invokedynamic 被用來調度這個調用:

public static void main (java.lang.String[]);
  Code:
   : new #4 // class com/zt/Main 3: dup          
   4: invokespecial #5 // Method "":()V 7: invokedynamic #6,  // InvokeDynamic 
#0:lambda:() Lcom/zt/Main$Action; 12: invokevirtual #7 // Method action:(Lcom/zt/Main$Action;)V 15: return

        而在常量池里,你也可以找到運行時的啟動方法:

BootstrapMethods:: #40 invokestatic java/lang/invoke/LambdaMetafactory.metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;
Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;
Ljava/lang/invoke/MethodType;) Ljava/lang/invoke/CallSite;
  Method arguments:
    #41 invokeinterface com/zt/Main$Action.run:(Ljava/lang/String;) Ljava/lang/Object;
    #42 invokestatic com/zt/Main.lambda$:(Ljava/lang/String;) Ljava/lang/Object;
    #43 (Ljava/lang/String;) Ljava/lang/Object;

        你會發現到處都是在使用 MethodHandle API,但我們現在不打算深入到里面。現在我們可以確認一點,我們的定義是引用了編譯出來lambda$0方法。

        我很好奇,如果我定義一個相同名字的靜態方法會怎樣——畢竟“lambda$0”是一個有效的標識符!于是,我定義了自己的lambda$0方法:

public static Object lambda$(String s){ return null; }

        而編譯失敗,編譯器不允許我在代碼了擁有這個方法:

java: the symbol lambda$(java.lang.String) conflicts with a 
           compiler-synthesized symbol in com.zt.Main

        同時,如果我刪掉這段定義 lambdas 表達式的代碼,程序能順利編譯通過。這就是說,lambdas 表達式在編譯期間會比類里的其它數據早先分析,不過這只是我的猜測。

        請注意:在這個例子中,lambda 并沒有去引用任何變量,也沒有引用類內部的任何方法。這就是為什么產生的lambda$0方法是靜態的。如果 lambdas 引用了上下文中的變量或方法,那生成的將是一個非靜態方法。所以,不要被這個例子誤導——lambdas 是可以捕獲上下文環境內容的!

        總結 lambdas

        我可以毫無疑問的說,lambdas 和伴隨它一起的各種特征(守衛方法(defender)

        ,升級的集合類庫)將很快給 Java 帶來巨大的沖擊。它的語法相當的簡單,一旦程序員們意識到這些功能給開發效率帶來的好處,我們將會看到大量的程序員都會運用這個功能。

        看看 lambdas 會編譯成什么樣子,這對于我來說是一件非常有趣的事情,我很開心,因為我看到這些所有的invokedynamic指令調用都沒有出現匿名內部類。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!