Java中的匿名内部类与lambda表达式及字节码执行区别

我们知道普通内部类、普通静态内部类和普通类的区别,他们在编译的时候,其实与外部类都编译 成了两个class文件,再看下匿名内部类和lambda表达式是如何编译实现的。

匿名内部类

这里创建一个Runnable匿名类,在主线程中执行,因为如果new Thread异步执行的话就创建了两个匿名内部类稍显复杂。
public class NoName {
    private int age = 10;
    public void testPrinter() {
        int height = 180;
        Runnable nothing = new Runnable() {

            @Override
            public void run() {
                if (System.currentTimeMillis() % 2 == 0) {
                    System.out.println(age);
                    System.out.println(height);
                } else {
                    System.out.println("nothing");
                }
            }
        };
        nothing.run(); // 简单点,直接在主线程中执行
//        height = 179;  // 这里放开会编译不通过
//        age = 11; // 这里放开可以正常编译
    }
}

我们用命令javap -c NoName.class 查看编译信息

Compiled from "NoName.java"
public class Test.NoName {
  public Test.NoName();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #1                  // Field age:I
      10: return

  public void testPrinter();
    Code:
       0: sipush        180
       3: istore_1
       4: new           #3                  // class Test/NoName$1
       7: dup
       8: aload_0
       9: iload_1
      10: invokespecial #4                  // Method Test/NoName$1."<init>":(LTest/NoName;I)V
      13: astore_2
      14: aload_2
      15: invokeinterface #5,  1            // InterfaceMethod java/lang/Runnable.run:()V
      20: return

  static int access$000(Test.NoName);
    Code:
       0: aload_0
       1: getfield      #1                  // Field age:I
       4: ireturn
}
这个编译信息还是 比较简单的,testPrinter方法中会先创建一个NoName$1对象,然后初始化后调用一个接口方法 invokeinterface 。
与NoName.class同级目录下还会有一个NoName$1.class文件

如果在idea中看不到可以去对应文件管理器中查看,可能因为某些设置被过滤掉了。
使用命令javap -c 'NoName$1.class'查看编译信息

Compiled from "NoName.java"
class Test.NoName$1 implements java.lang.Runnable {
  final int val$height;

  final Test.NoName this$0;

  Test.NoName$1(Test.NoName, int);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:LTest/NoName;
       5: aload_0
       6: iload_2
       7: putfield      #2                  // Field val$height:I
      10: aload_0
      11: invokespecial #3                  // Method java/lang/Object."<init>":()V
      14: return

  public void run();
    Code:
       0: invokestatic  #4                  // Method java/lang/System.currentTimeMillis:()J
       3: ldc2_w        #5                  // long 2l
       6: lrem
       7: lconst_0
       8: lcmp
       9: ifne          38
      12: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: aload_0
      16: getfield      #1                  // Field this$0:LTest/NoName;
      19: invokestatic  #8                  // Method Test/NoName.access$000:(LTest/NoName;)I
      22: invokevirtual #9                  // Method java/io/PrintStream.println:(I)V
      25: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_0
      29: getfield      #2                  // Field val$height:I
      32: invokevirtual #9                  // Method java/io/PrintStream.println:(I)V
      35: goto          46
      38: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      41: ldc           #10                 // String nothing
      43: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      46: return
}

可以看到这个就是匿名内部类生成的class信息,其将外部类的对象引用NoName,和局部变量height都作为构造函数的参数传入,并且是final保存的,这也解释了为什么上面的 height=19 这一行放开的话会编译不过,因为传入匿名内部类的局部变量参数是final修饰的,
其实这里还是没能解释为什么局部变量有这个要求,必须是final类型或者effectively final 类型的(也就是只能改变一次)。局部变量的存储是在方法栈帧中的,生命周期比较短,很可能主方法执行完了,而匿名内部类的方法还在执行中,所以局部变量的传入是复制一份值(基本数据类型是复制值,引用类型则复制引用地址)到匿名内部类中。所以你可以像这样修改传值,做一些骚操作。

public class NoName {
    private int age = 10;

    public void testPrinter() {
        ArrayList<String> list = new ArrayList<>();
        Runnable nothing = new Runnable() {

            @Override
            public void run() {
                if (System.currentTimeMillis() % 2 == 0) {
                    System.out.println(age);
                    System.out.println(list);
                } else {
                    System.out.println("nothing");
                }
            }
        };
        nothing.run();
        list.add("hello");
//        list = new ArrayList<String>();  // 不可以这样写,改变了引用的值
    }
}

而成员变量age的获取是通过外部实例对象的引用获取的,并没有发生复制,内外访问的都是同一个变量,而且外部实例对象的值存储在堆上,生命周期长,所以不需要是final的。
此外,假设如果Java允许传入非final的值,放开注释,如果runable使用new Thread放在异步线程中执行的话,那runable里面应该打印18还是19呢?
Java为了避免这种二义性,直接禁止掉了 这种场景,要求传入匿名内部类的参数必须是final或者effective final。
所以总结一下,因为局部变量的生命周期短,所以传入匿名内部类时时复制了一个值进去,这个值用final修饰,而外部成员变量是通过对象实例寻找获取。同时为了保证数据逻辑的一致性,避免二义性

这里有个问题,就是二义性,既然局部变量会导致,成员变量也会有。但是Java是允许这种行为的,因为成员变量是通过引用地址去获取成员变量的,两边修改和访问的都是同一个变量,但是局部变量是值复制,两边是值复制,并不是同一个"变量",Java不允许“看起来是共享变量,实际上是复制变量”
和普通类比较:
匿名内部类是一次性的,无法重复使用,普通内部类其实还是一个比较完整的类。可以实现接口(没有方法个数限制),或者继承一个类(可以是抽象类或者普通类),但是只能继承一个类或者实现一个接口。匿名内部类还不能有构造函数。

lambda表达式

public class NoName {
    private int age = 10;

    public void testPrinter() {
        int height = 190;
        Runnable nothing = () -> {
                if (System.currentTimeMillis() % 2 == 0) {
                    System.out.println(age);
                    System.out.println(height);
                } else {
                    System.out.println("nothing");
                }
        };
        nothing.run();
//        height = 180;
    }
}

这次编译后,只会有一个class文件,具体编译信息如下

Compiled from "NoName.java"
public class Test.NoName {
  public Test.NoName();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field age:I
      10: return

  public void testPrinter();
    Code:
       0: sipush        190
       3: istore_1
       4: aload_0
       5: iload_1
       6: invokedynamic #3,  0              // InvokeDynamic #0:run:(LTest/NoName;I)Ljava/lang/Runnable;
      11: astore_2
      12: aload_2
      13: invokeinterface #4,  1            // InterfaceMethod java/lang/Runnable.run:()V
      18: return
}

与匿名内部类一样,在访问局部变量时需要该变量是final或effectively final。但是从指令上可以看出,匿名内部类在执行时时先创建这个对象,然后调用它的方法。lambda表达式则是调用了** invokedynamic**指令。

lambda表达式和匿名内部类对比

  1. lambda只能实现只有一个抽象方法的接口,也就是函数式接口,不会生成一个新的类。匿名内部类本质时创建一个新的类的实例对象,可以定义成员变量。
  2. 通过JVM调用invokedynamic指令在运行时实现
  3. lambda在实现时代码更简洁,可以自动判断泛型
Comparator<Integer> c = (a, b) -> a-b;
Comparator<Integer> c = new Comparator<Integer>() {
        @Override
        public int compare(Integer a, Integer b) {
            return a - b;
      }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容