1. 程式人生 > >JAVA 8:Lambdas表示式初體驗

JAVA 8:Lambdas表示式初體驗

原文連結譯文連結,譯者:鄭旭東

Lambdas專案是即將釋出(譯者注:原作者寫本文的時候JAVA8尚未釋出)的JAVA8中重要主題,同時它應該也是眾多JAVA開發者最期待的功能。還有一個非常有意思的功能同Lambda表示式一起被加入到了JAVA中,它就是Defender方法。在這篇博文中,我想去探究一些更深層次的東西——JAVA如何在執行期表達Lambda表示式的和那些位元組碼指令在方法排程時被呼叫。

雖然JAVA8尚未釋出,但你仍然可以通過“下載未正式釋出版本”來體驗JAVA8的魅力。

你想使用Lambdas?

如果你對其他包含Lambdas表示式的程式語言比較熟悉,如Groovy和Ruby,你可能會對它們並不像JAVA中那樣簡單而感到驚訝。在JAVA中,Lambda表示式是一個“SAM type”(譯者注:SAM即Single Abstract Method),是一個只包含一個抽象方法的介面(是的,現在介面可以可以包含非抽象方法——Defener方法)。

舉個例子,Runnable介面就是能很好的作為SAM型別:

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

或者,我們也可以同樣的使用Comparable介面:

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

同樣,也可以這樣寫:

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

因此,一個Lambda表示式似乎有一個隱含的return語句。

如果我想編寫一個可以接受 Lambda表示式作為引數的方法將要怎麼做?首先你必須先宣告一個Functional介面的引數,然後你就可以傳入一個Lambda表示式了。

當我們有了一個可以接受Functional介面作為引數的方法後,我們可以這樣呼叫它:

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

實際上,相同的表示式可以被替換為一個方法引用,因為它只是一個使用相同引數的單一方法呼叫:

  execute(System.out::println);

然而,若方法的引數存在任何的轉換操作,我們將不能使用方法引用,必須使用完整的Lambdas:

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

我認為這個語法是相當不錯的。現在,我們在JAVA中有了相當優雅的Lambdas表示式解決方案,即使JAVA本身不具備functional型別。

JDK8中的Functional介面

我們明白,Lambda表示式在執行期被表示為一個functional介面(或者一個“SAM型別”)。並且雖然JDK已經包含了若干符合SAM標準的介面,如Runnable和Comparable,但這對於API的演進是明顯不夠的。因為在程式碼中肆意使用Runnable也是不可以接受的。

在JDK8中出現了一個新的包,java.util.function,它包含了一些可以在新的API中使用的functional介面。我們將不會在這裡把它們都列出來,你可以稍後自己研究下這個包:)

似乎目前API演進得相當快,有些介面被加入了又被刪除。比如,原來的JDK8提供了java.util.function.Block類,但當我寫這篇文章時,最新的JDK8版本把這個類移除了。

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

而後,我發現它被新的Consumer介面代替了並被所有collections庫的新新增的方法所使用。舉個例子,Collection介面定義的forEach方法如下:

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

比較令人感興趣的一點是Consumer介面只定義了一個抽象方法——accept(T t),和一個defener方法——Consumer<T> chain(Consumer<? extend T> consumer)。這意味著有可能使用該介面進行鏈式呼叫。我不太清楚如何使用它,因為我沒有在JDK中找到使用它的地方。

我也發現所有這些介面都帶有@FunctionalInterface註解。除了在執行時的作用,這個註解還用於javac校驗介面是否是真正的functional介面並且只定義了一個抽象方法。

若我們嘗試編譯這樣的程式碼

@FunctionalInterface
interface 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

然而如下的程式碼是可以通過編譯的:

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

反編譯Lambdas

我通常不對語法和語言的功能感到好奇,我更關心它們執行時的表達。這就是為什麼拿起我心愛的javap工具開始閱讀包含Lambda表示式的類的位元組碼對我來說是一件很自然的事情。

當前(JAVA 7或者以前),如果你想在JAVA中模擬Lambdas表示式,你不得不宣告一個匿名的內部類。這將導致在編譯後出現該類專有的class檔案。並且如果你有多個這樣的類,這些檔案的檔名後將會有一個數字字尾。那麼Lambda是怎麼樣的呢?

請看下下面的程式碼:

public class Main {
 
 @FunctionalInterface
 interface Action {
   void 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中必須有Lambda表示式的實現的表示。

$ 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$0(java.lang.String);
}

看,編譯器在我們反編譯的類中產生了一個lambda$0。使用-c -v選項將為我們展示真正的位元組碼。

main方法揭示了invokedynamic被用於調動方法呼叫:

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

並且在常量池中可以找到bootstrap方法在執行期會和它相關聯

BootstrapMethods:
 0: #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$0:(Ljava/lang/String;)Ljava/lang/Object;
 #43 (Ljava/lang/String;)Ljava/lang/Object;

你可以看到MethodHandle API在這裡被廣泛的應用,但是我們不在這裡討論它。現在,我們可以確定這些定義同lambda$0有關。

我好奇的是,如果我定義了一個名為lambda$0的靜態方法後會如何。

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

當我編譯時編譯器會提示如下錯誤,它不允許我定義這樣的一個方法

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

於此同時,若我刪除了定義了Lambda表示式的程式碼後,這個程式碼就可以正常編譯了。這實際上是告訴我們lamdba在其他結構編譯之前就被捕獲了,但這僅僅是我的假設。

請注意,在這個例子中Lambda表示式並沒有捕獲任何變數和引用類中的任何方法。這就是lambda&0方法為什麼是靜態的原因。如果它引用了類中任何一個變數或者方法,它將不會是一個靜態的方法。因此請不要被這個例子誤導。

總結

我們可以明確的說Lambda還有與其相關的一些功能將對JAVA產生深遠的影響。它的語法十分棒並且一旦開發人員意識到這些功能將提高他們的生產力時,我們將會看到越來越多使用這些功能的程式碼。

我對Lambda編譯後的樣子十分感興趣並且我也相當高興我看到了invodkeynamic指令在完全沒有匿名內部類 參與的情況下的應用。