Lambda表达式与匿名内部类的区别分析

# 背景
最近面试中与几个候选人探讨过类似的问题,发现多数人对这一概念仍然比较模糊,本文将从字节码的角度简单介绍一下两者的区别


# 匿名内部类
匿名内部类其实就是没有名字的内部类,但是其必须要实现一个接口或者继承一个父类,通常是用来简化代码


例程如下:
先定义一个IAnimal
```java
public interface IAnimal {
    void run();
    void walk();
}
```


然后定义测试类NestedClassTest
```java
public class NestedClassTest {
    public static void main(String[] args) {
        IAnimal pig = new IAnimal() {
            @Override
            public void run() {
                System.out.println("pig is running");
            }


            @Override
            public void walk() {
                System.out.println("pig is walking");
            }
        };


        pig.run();
    }
}
```


进入classes目录,我们可以发现出现了三个class文件
IAnimal.class
NestedClassTest.class
NestedClassTest$1.class


显然NestedClassTest$1就是匿名内部类了,现在使用javap指令反编译NestedClassTest.class可以得到如下字节码
```
public class com.alios.d.NestedClassTest {
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // 解析运行时常量池中NestedClassTest$1的类符号引用,创建对象,并将对象引用推入栈顶
       3: dup                                 // 复制栈顶变量
       4: invokespecial #3                  // 调用NestedClassTest$1."<init>"方法
       7: astore_1
       8: aload_1
       9: invokeinterface #4,  1            // 调用接口方法IAnimal.run
      14: return
}
```


从上文的字节码可以看出,匿名内部类的调用方法与普通类没有区别
* 编译器会为每个匿名类生成一个新的class文件,文件名格式为className$num。


如果Lambda表达式也采用类似方式,将是极为不利的。
* 类加载需要有加载、验证、准备、解析、初始化等过程,大量的内部类将会影响应用执行的性能,并消耗Metaspace


# Lambda表达式
接下来,我们看一下Lambda表达式
先上代码,我们先定义一个函数式接口


```java
@FunctionalInterface
public interface IBird {
    void fly();
}
```


然后定义一个Lambda表达式的测试类
```java
public class LambdaTest {
    public static void main(String[] args) {
        IBird seagull = () -> {
            System.out.println("seagull is flying");
        };
        seagull.fly();
    }
}
```


进入classes目录,我们可以发现仅有两个class文件
IBird.class
LambdaTest.class


使用javap指令反编译LambdaTest.class可以得到如下字节码
```
public class com.alios.d.LambdaTest {
 Constant pool:
   #1 = Methodref          #8.#25         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#30         // #0:fly:()Lcom/alios/d/IBird;
   #3 = InterfaceMethodref #31.#32        // com/alios/d/IBird.fly:()V
   #30 = NameAndType        #42:#43        // fly:()Lcom/alios/d/IBird;


  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // 动态调用指令
       5: astore_1
       6: aload_1
       7: invokeinterface #3,  1            // 调用接口方法IBird.fly
      12: return
}
```


从上文字节码可以看出Lambda采用的是invokedynamic指令,而非构建一个新的class。


## invokedynamic指令
invokedynamic指令是JDK 1.7 JSR 292 引入的,当时的目的是为了支持Groovy、JRuby等动态类型语言,但是在JDK 1.8中,该指令又被用到了Lambda表达式实现中。
```
invokedynamic indexbyte1 indexbyte2 0 0
```
根据JVM 1.8规范,invokedynamic有4个操作数,前两个操作数构成一个索引[ (indexbyte1 << 8) | indexbyte2 ],指向类常量池,后两个操作数为保留字。
从上文字节码可以看出,invokedynamic的操作数指向了常量池的CONSTANT_InvokeDynamic_info结构


invokedynamic执行步骤如下:
* invokedynamic指令行,被称为动态调用点
* 当首次执行该invokedynamic调用时,JVM会调用一个bootstrap方法,并返回一个CallSite的对象,这个CallSite对象将永久与此动态调用点关联
* 执行与CallSite关联的MethodHandle指向的方法


使用invokedynamic指令实现Lambda表达式,带来如下的好处:
* 开销少,没有了匿名内部类的初始化过程
* 仅Lambda表达式首次调用的时候,进行转换和链接;之后的调用都会跳过这一步骤




# 参考
JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform
https://jcp.org/en/jsr/detail?id=292


JSR 335: Lambda Expressions for the JavaTM Programming Language
https://www.jcp.org/en/jsr/detail?id=335

猜你喜欢

转载自blog.csdn.net/a860MHz/article/details/81028882