和Lambdas的第一次亲密接触

标签: Java评论 Lambdas | 发表时间:2013-02-25 00:01 | 作者:Aqee
出处:http://www.aqee.net

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<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表达式作为参数的方法呢?这样,你需要先把这个参数声明成函数式的接口,然后把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里已经有了不少这样的接口,例如 RunnableComparable ,它们符合这种标准,但很显然,对于一个新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.0-ea-b75)
OpenJDK 64-Bit Server VM (build 25.0-b15, mixed mode)

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

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

Consumer接口里一个有趣地方是,它实际上定义了一个抽象方法—— accept(T t)和一个defender方法—— Consumer<T> chain(Consumer<? extend T> consumer)。这就是说你可以链式调用这个接口。我不确定如何使用,因为我在JDK包里没有找到 chain(..)的使用方法说明。

我还发现所有的接口都使用了 @FunctionalInterface运行时注注解注释。这个注释不仅仅是个说明,它还被javac使用来验证这个接口是否真是一个函数式接口,是否至少有一个抽象方法在里面。

所以,如果我们来编译下面的这段代码

@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工具,开始查看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.classMain$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$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

而在常量池里,你也可以找到运行时的启动方法:

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”是一个有效的标识符!于是,我定义了自己的 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

同时,如果我删掉这段定义lambdas表达式的代码,程序能顺利编译通过。这就是说,lambdas表达式在编译期间会比类里的其它数据早先分析,不过这只是我的猜测。

请注意:在这个例子中,lambda并没有去引用任何变量,也没有引用类内部的任何方法。这就是为什么产生的 lambda$0方法是 静态的。如果lambdas引用了上下文中的变量或方法,那生成的将是一个 非静态方法。所以,不要被这个例子误导——lambdas是可以捕获上下文环境内容的!

总结lambdas

我可以毫无疑问的说,lambdas和伴随它一起的各种特征(守卫方法(defender)
,升级的集合类库)将很快给Java带来巨大的冲击。它的语法相当的简单,一旦程序员们意识到这些功能给开发效率带来的好处,我们将会看到大量的程序员都会运用这个功能。

看看lambdas会编译成什么样子,这对于我来说是一件非常有趣的事情,我很开心,因为我看到这些所有的 invokedynamic指令调用都没有出现匿名内部类。


本文由 外刊IT评论网( www.aqee.net)原创发表,文章地址: 和Lambdas的第一次亲密接触

相关 [lambdas 第一次 亲密] 推荐:

和Lambdas的第一次亲密接触

- - 外刊IT评论
Lambda工程是即将到来的Java8的一大主题,可能也是程序员们最期待已久的东西. 随着Java lambdas的到来,还有一个有趣的东西被附带的加进了Java语言——defender(守卫者)方法. 在这篇文章里,我的目的是要看看面纱后的东西——看看在运行时环境里lambdas是表现的,在方法的调度过程中涉及到哪些字节码指令.

番外16★第一次

- Tony - 叫兽与你同在
七夕节的一个简单短片,没有动用剧组. 剧本是来自于我之前自己写的一条微博,所以就是我和老婆两个人完成.

第一次看见雪的企鹅

- yiwu - 河蟹娱乐
原文链接: http://hxyl.net/2011/01/08/xue-qi-e/. 火星娱乐 河蟹娱乐 Copyright © 2007-2009 爱祖国,爱人民,唉派对. 更多精彩欢迎您订阅http://feed.feedsky.com/kisshi,更加欢迎投稿. 稳定快速独立博客主机破盘价只要100元.

第一次负责项目总结

- lee - 博客园-首页原创精华区
         最讨厌的就是写总结,就是想写一下自己的观点和感受. 以下观点没有先后顺序,只是按照自己的大脑想到的先后顺序写下来的. 大家可能觉得这些观点书上都有,别人也说过,当然你也会发现扯蛋的观点到处都是,现在哥就扯给你看. 在开发一个新项目时一定要把框架搭好. 搭个鸡窝你也要考虑选址,材料,大小等.

我第一次谈人工智能

- - IT瘾-dev
不知道谁这么坏,给起了一个人工智能这个词,让通过科幻片才了解啥叫智能的普罗大众老误解人工智能,就像现在我仍然对智能手机这个词有微言,这怎么就智能了呢. 从现在表现出来的人工智能规模应用来说,本质上就是个概率统计的事,和智能根本不沾边,倒是和人工挺沾边,需要人工做大量的数据标准化加工、数据标注,再喂给算法.

2011年1月HZLUG第一次线下活动总结+照片

- cantrip - Linux桌面中文网
HZLUG的第一次线下活动已经结束了,非常给力,完全超出我的意料. 本来大概估计着来个30几个的,没想到热情的大家都千里迢迢来到了远在滨江、N多杭州出租车司机都不知道的杭州网易来参加活动. 后来一共来了60人左右,会议室都满了,网易的哥们还帮搬了很多十多把椅子……. 这里我就不一一感谢了,感谢所有今天来参加活动的同学们,就在这里简单的记录下今天活动的过程.

第一次 Google+ 就上手,超簡單入門指南

- Elic - T客邦
Google+ 的熱度雖然不像剛開始這麼高,但還是有不少人的網路重心都往 Google+ 偏移,同時也還是有不少新加入的 Google+ 使用者. Saidur (Cy) Hossain就製作了一份 Google+ 的入門使用教學,目前已經有不少翻譯. 正體中文則是由Zhusee Zhang翻譯,下面就直接看圖說明吧.

Google 无人驾驶汽车出现第一次撞车事故

- lackar - 36氪
Google CEO Eric 说:「汽车先于电脑发明是一个 Bug. 」然后我们就知道2009年时,Google的无人驾驶汽车已经在路上跑了. 根据Jalopnik,本周早些时候,Google 的无人驾驶汽车在山景城追尾了另一辆汽车(上图),这似乎是 Google 无人驾驶汽车的第一次撞车事故. Google 发言人对Business Insider 说这次事故发生的时候,车里是有驾驶员在控制汽车的:.

我第一次听这个笑话的时候

- 纸条 - 新闻跟帖局
网易体育:周雨薇的美貌,让不少男嘉宾都将她作为自己的心动女生. 乐嘉在介绍她时,这样评价道:“她曾经是河南省女足的运动员,因为太漂亮了被队内排斥,所以最终放弃了足球. ”[查看原文]好友向您推荐这条跟贴. 关注关注他的微博村长领导 [网易美国网友]:2011-08-29 18:18:57 发表网易江苏省淮安市网友 [鸿沟道人] 的原贴:1.

算是第一次正式面实习吧。

- sun - 天空之珵
之前MSRA的是内推,mentor给我打了个电话聊聊人生聊聊前途…… 这次是Google London. 面的原因是Google的猎头看到我简历然后 打电话来问有没有兴趣面个伦敦暑假实习. 不知道为什么我都没看到过英国的中国人去面google london/dublin/zurich ……. "别紧张,放轻松,都读到PHD了,快速排序你会吧.