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指令在完全没有匿名内部类 参与的情况下的应用。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/140653.html

(0)
上一篇 2021年9月5日
下一篇 2021年9月5日

相关推荐

发表回复

登录后才能评论