本文通过分析编译后的class文件,来揭秘Java中内部类的实现原理。
在解开内部类的神秘面纱之前,让我们先来看如下代码:
1 | public class OuterClass { |
上述程序编译之后生成了如下两个class文件:
1 | OuterClass$InnerClass.class |
可以看到,编译器将内部类InnerClass翻译成了名为OuterClass$InnerClass.class
的class文件。
下面我们在命令行中使用javap
命令来查看下编译之后的OuterClass$InnerClass.class文件(关于javap
的更多描述请参看《Java中的命令行工具javap》)
1 | //在UNIX中,需要对$转义,其他系统中不需要加\ |
输出内容如下:
1 | Compiled from "OuterClass.java" |
可以清楚地看到,编译器为了引用外部类,生成了一个使用final修饰的外部类变量,命名为this$0
(名字this$0
是由编译器合成的,在自己编写的代码中不能够引用它)。
另外,可以看到构造器的参数,在InnerClass中,我们并没有写构造方法,编译器为InnerClass自动生成了一个带参数的构造方法,传入一个外部类对象的引用。
开篇提到,内部类可以访问外围类中的所有方法和变量,即使这些方法和变量声明为private。既然内部类可以被编译器翻译为名字古怪的普通类(虚拟机对此一无所知),内部类如何管理那些额外的访问特权呢?
再次使用javap命令查看生成的Outerclass.class
,结果如下:
1 | Compiled from "OuterClass.java" |
我们注意到,编译器在外围类中添加了两个静态方法access$0
和access$1
,它们均需要传入一个外围类对象的引用。此外,access$1
还需要传入一个字符串变量。(方法名可能稍有不同,如access$00
,这取决于你的编译器。)
内部类通过调用access$0
方法,从而实现对外围类的私有属性name的访问;通过调用access$1
方法,从而实现对外围类的私有属性name的修改。
如果编译器能够自动地进行转换,那么我们能不能编写程序实现这种机制呢?
1 | public class Main { |
输出结果:
1 | Outer初始化 |
可以看到,通过模仿编译后的class文件中的代码,在Outer中添加一些特殊的方法,在Inner类中添加特殊的构造器,我们在Inner类中访问和修改了Outer类中的私有属性。