背景
最近遇到了一种稀奇的匿名内部类写法,代码如下:
String name = "小明"
ArrayList<String> arrayList = new ArrayList<String>() {
{
add(name);
}
};
仔细一看这不是代码块么?原来还可以在匿名内部类里写代码块,真的是头一次见。
分析
但是很快我就发现了不对劲,具体分析结合下面的代码:
// 写法1
ArrayList<String> arrayList = new ArrayList<String>() {
{
add("Hello,World!");
}
};
// 写法2
String name = "小明"
ArrayList<String> arrayList2 = new ArrayList<String>() {
// 这里虽然可以通过编译,但是由于我们使用ArrayList类型接收,所以test()是无法被调用的
public void test(){
System.out.println(name);
}
};
// 写法3
String name = "小明"
ArrayList<String> arrayList = new ArrayList<String>() {
{
add(name);
}
};
-
写法1非常好理解,匿名内部类里写了一段代码块,没什么毛病。
-
写法2也非常好理解,将外部变量name传入到匿名内部类的成员方法中,也没什么毛病。如果你知道匿名内部类的原理,那你也会觉得非常好理解,即匿名内部类经过编译后,会独立生成一个Class文件,而我们传入的变量值会作为这个新的类中的成员变量,同时编译器也会根据我们传入匿名内部类的外部变量,来生成对应的构造方法供外部使用。
- 注意!!!外部传入的匿名内部类的变量,不管是在内部还是在外部,都是要final修饰的,不要被JDK的语法糖给骗了(JDK8允许我们不显式的加上final,编译器会替我们加上),因为在原理上外部的变量和匿名内部类的变量已经不是一个变量了,只是同一个引用,所以需要使用final修饰,保证两者始终都是同一个引用。至于JDK为什么要这么做,我认为是一种语义上的限定,并不是说不被final修饰就不能实现代码的运行,感兴趣的朋友可以自行查阅。
-
写法3有点让人不能理解了,将外部变量name传入到匿名内部类的代码块中,这个行为让人非常疑惑!看起来好像和写法2差不多,但是差了非常多!!!
- 如果你了解Java创建对象的流程的话,你就会知道代码块的执行顺序是优先于构造方法的,写法2中的外部变量是在构造方法之后执行的,所以匿名内部类可以让编译器生成对应的构造函数,让调用者调用这个构造函数即可完成外部变量给匿名内部类中的成员变量赋值的操作。但是写法3的问题在于,代码块的执行优先于构造函数,就算我们给匿名内部类设置了外部变量对应的成员变量的值,并且在对应的构造函数中进行赋值操作,在代码块中对匿名内部变量类中成员变量name都还没有被赋值,是一个null值,这是不符合匿名内部类的原理的。
实践
存在即合理,既然写法3的代码可以运行,那一定是它的道理。但是这对我来说还是一个未知的谜,既然如此,只好上字节码了!字节码会告诉我们这一切的答案!!!
代码
public class DemoController {
{
System.out.println("1...");
System.out.println("2...");
System.out.println("3...");
}
public DemoController(){
System.out.println("DemoController()...");
}
public DemoController(String code){
System.out.println("DemoController(" + code + ")...");
}
public static void main(String[] args) {
String code1 = "双击666";
String code2 = "双击666";
DemoController demoController1 = new DemoController(){
{
System.out.println("初始化代码块...");
}
};
DemoController demoController2 = new DemoController(){
{
System.out.println(code1);
System.out.println("初始化代码块...");
}
{
System.out.println(code2);
}
};
DemoController demoController3 = new DemoController("双击666"){
{
System.out.println(code1);
System.out.println("初始化代码块...");
}
{
System.out.println(code2);
}
};
Scanner scanner = new Scanner(System.in);
scanner.nextInt();
}
}
对应字节码
根据上图可以得出,我们传入的外部变量code1
、code2
,编译器替我们收集到了对应的<init>
方法中,作为参数传入了<init>
方法。
到这里,其实我就已经有种悟了的感觉。我猜测由编译器收集的<init>
方法是由类中的代码块和构造函数组成的:
-
<init>
方法由相同的代码块内容 + 每个构造函数组成(也就是我们有几个构造函数,就有几个<init>
方法,它们都有相同的代码块内容) -
<init>
方法的入参,由构造函数的入参和匿名内部类的代码块中传入的外部变量决定 -
<init>
方法中,代码块的代码始终在构造函数前面执行
什么?你说你看不懂字节码?没事!有我在!我反编译给你看!!!
根据上图,比如DemoController$2
、DemoController$3
的Class文件,可以看到对应的<init>
方法中,在对外部变量对应的内部变量进行操作之前,提前对变量进行了赋值操作。这验证了我的猜测。
- 眼尖的同学肯定看到了编译后的代码中,外部变量和匿名内部类中的内部变量都被final修饰了,你真棒!!!
总结
编译器收集的<init>
方法是由类中的代码块和构造函数组成的:
-
<init>
方法由相同的代码块内容 + 每个构造函数组成(也就是我们有几个构造函数,就有几个<init>
方法,同时它们都有相同的代码块内容) -
<init>
方法的入参,由构造函数的入参和匿名内部类的代码块中传入的外部变量决定 -
<init>
方法中,代码块的代码始终在构造函数前面执行
经过这次的探索,我又学到了新的知识:<init>
方法是由代码块和构造方法共同组成的(以前我认为<init>
方法就是一比一对应构造函数的,而代码块就是由编译器收集成另一个方法,在<init>
方法之前执行的,哈哈哈)。路还很长,我们一起继续向前进!!!加油!加油!加油!