我们知道普通内部类、普通静态内部类和普通类的区别,他们在编译的时候,其实与外部类都编译 成了两个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表达式和匿名内部类对比:
- lambda只能实现只有一个抽象方法的接口,也就是函数式接口,不会生成一个新的类。匿名内部类本质时创建一个新的类的实例对象,可以定义成员变量。
- 通过JVM调用invokedynamic指令在运行时实现
- 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;
}
};