JDK 8新特性—Lambda表达式

Lambda表达式

简介:

Lambda表达式本质上是一种匿名方法,能将该方法作为参数传入其他方法中,用法上与一个只需实现一个方法的匿名内部类很相似,Lambda表达式能完全取代这种匿名内部类的位置,且拥有着更简洁的写法和更好的可读性。

用法:
//  ( /*参数列表*/ )->{ /*具体实现*/ }
public class MyClass {
    String s="OutString";
    public static void main(String[] args){
        //  比较常用的进程的定义
        // 匿名内部类的写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                //具体实现
                s="change";    
                // 使用外部成员时,会隐式地将其指定为final类型,无法更改其值,故该部分错误
            }
        }).start();

        // Lambda表达式
        new Thread(()->{
            //具体实现
        }).start();

        //需要参数的情况,在此定义一个Button button=new Button();
        button.setOnClickListener((v)->{    //能识别形参的参数类型,多个参数用逗号隔开
            //具体实现
        });

        test(()->s);    //当只有一行代码时不需要“{}”,且能自动识别返回类型
    }
    public static void test(testClass s){}
}
interface testClass{
    String m();
}

Lambda表达式与匿名类的区别:

如果只是想知道什么场合、怎么使用Lambda表达式,上面的部分就足够了,接下来将对Java是如何具体实现Lambda表达式和匿名内部类做出介绍,会发现,两者有着本质的不同。

静态类型语言与动态类型语言

Java是静态类型语言,举个栗子:

obj.println("Hello world");

显然,这行代码需要一个具体的上下文才有讨论的意义,假设它在Java语言中,并且变量obj的类型为java.io.PrintStream,那obj的值就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj拥有一个println(String)方法,但与PrintStream接口没有继承关系,代码依然不能运行——因为类型检查不合法。
  但是相同的代码在动态类型语言ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,只要这种类型的定义中确实包含有println(String)方法,那方法调用便可成功。
  这种差别产生的原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本例中为一项CONSTANT_InterfaceMethodref_info常量)生成出来,作为方法调用指令的参数存储到Class文件中,例如下面这个例子:

public void print(){
    PrintStream printStream=System.out;
    printStream.println("Hello world");
}
//对应的.class文件,在命令行中使用“javap -c *.class”命令查看
public void print();
  Code:
     0: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: astore_1
     4: aload_1
     5: ldc           #3  // String Hello world
     7: invokevirtual #4   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     10: return

这个符号引用(Code的“7: invokevirtual ……”部分)包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有的类型,编译时候最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。

Java的动态语言实现

JDK 7时Java在虚拟机层面提供了动态类型的直接支持:invokedynamic和java.lang.invoke包,为Lambda表达式的出现奠定了基础。
  在此对invokedynamic指令做简单介绍:每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。

invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V   
//“#123”表示从常量池第123项获取常量(CONSTANT_InvokeDynamic_info常量)
//0表示BootstrapMethods属性表第0项
//其内容则是“testMethod:(Ljava/lang/String;)V”
Lambda表达式的本质

Java无法将方法作为参数传递给其他方法,因此大多数情况下会定义一个包含一个抽象方法的接口用来作为方法的载体。Lambda表达式弥补了这一点,Lambda表达式的本质是一个抽象方法,可以将其作为参数传递。

public class MyClass {
    public void print (){
        new Thread(()-> {}).start();    //使用Lambda表达式
    }
}
//其对应的字节码
public class com.example.MyClass {
  public com.example.MyClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void print();
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      12: invokevirtual #5                  // Method java/lang/Thread.start:()V
      15: return
}
4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

此为Lambda表达式对应的字节码,可以看到这里调用run方法,返回java.lang.Runnable类型的对象,然后以此构建Thread对象。

匿名内部类与Lambda表达式的对比

观察匿名内部类的实现:

public class MyClass {
    public void print (){
        new Thread(new Runnable() {
            @Override
            public void run() {}
        }).start();
    }
}
//MyClass.class其对应的字节码
public class com.example.MyClass {
  public com.example.MyClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void print();
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: new           #3                  // class com/example/MyClass$1
       7: dup
       8: aload_0
       9: invokespecial #4                  // Method com/example/MyClass$1."<init>":(Lcom/example/MyClass;)V
      12: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      15: invokevirtual #6                  // Method java/lang/Thread.start:()V
      18: return
}
//MyClass$1.class其对应的字节码
class com.example.MyClass$1 implements java.lang.Runnable {
  final com.example.MyClass this$0;

  com.example.MyClass$1(com.example.MyClass);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lcom/example/MyClass;
       5: aload_0
       6: invokespecial #2                  // Method java/lang/Object."<init>":()V
       9: return

  public void run();
    Code:
       0: return
}

从中可以看出,匿名内部类首先构造了一个MyClass$1类的对象(该类继承Runnable接口),然后使用该对象构造Thread对象,事实上,在MyClass.class文件所在的文件夹中同时存在MyClass$1.class文件。由此可看出,匿名内部类的实现就是由编译器构建一个继承对应接口的类。
  由此可得知Lambda表达式是方法,匿名内部类是类,明白了这些,很多事情就简单了,如:两者如何接受外部类变量和外部方法变量?
  Lambda以方法参数的形式接收:

public class MyClass {
    String classString="Hellow";    //外部类变量
    public void print (){
        String functionString="World";    //外部方法变量
        new Thread(()->{
            String a=classString+functionString;
        }).start();
    }
}
//对应字节码
  public com.example.MyClass();
    Code:
       ……

  public void print();
    Code:
       ……
       9: invokedynamic #6,  0              // InvokeDynamic #0:run:(Lcom/example/MyClass;Ljava/lang/String;)Ljava/lang/Runnable;
      //run方法多了两个参数,一个指向外部类,一个指向外部方法中的String变量
      14: invokespecial #7                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      17: invokevirtual #8                  // Method java/lang/Thread.start:()V
      20: return
}

内部类则是将变量添加入自身属性列表:

class com.example.MyClass$1 implements java.lang.Runnable {
  final java.lang.String val$functionString;    
  //外部方法中的String变量对应的常量,内部使用时才存在

  final com.example.MyClass this$0;    
  //指向MyClass的常量一直存在,不管内部有没有使用外部类的属性

  com.example.MyClass$1(com.example.MyClass, java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lcom/example/MyClass;
       5: aload_0
       6: aload_2
       7: putfield      #2                  // Field val$functionString:Ljava/lang/String;
      10: aload_0
      11: invokespecial #3                  // Method java/lang/Object."<init>":()V
      14: return

  public void run();
    Code:
       ……
      28: return
}

对比之前方法体为空的字节码可以看出,内部类会一直有一个指向外部类的指针,而Lambda表达式只有在使用到外部类的属性时,才会接收该参数。

Q&A:

Lambda表达式和匿名类均能直接访问外部方法的局部变量和外部类的属性,但访问时却隐式的指定所访问的成员是final(访问外部类的属性是通过指向外部类的指针完成的,该指针是final类型),为什么?

匿名内部类访问外部成员时,会将所访问的成员复制到类内),其原因是所访问的成员与匿名内部类生命周期的不同,当方法执行完毕后,局部变量就会被清理,但匿名类可能依旧存在,因此需要将其复制到内部。为了保证复制品与原始变量的一致,将其指定为final类型(Lambda表达式也是同样的道理)。JDK 8之后,检测条件宽松了许多,只要所使用的属性在之后的代码中,其值不会改变即可,不一定非要指定为final,但在JDK 8之前,外部方法中的局部变量需定义为final。

参考资料:

周志明.深入理解Java虚拟机——JVM高级特性与最佳实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容