javac在编译创建内部类对象时生成的奇怪的getClass()调用是什么?有人问下面这段代码里,main()方法里的oute
javac在编译创建内部类对象时生成的奇怪的getClass()调用是什么?
有人问下面这段代码里,main()方法里的outer.new Inner()部分为什么会生成了一个对outer.getClass()的调用:
public class Outer { public class Inner { } public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); }}javac编译它生成的main方法的代码是:
public static void main(java.lang.String[]); Code: Stack=4, Locals=3, Args_size=1 0: new #2; //class Outer 3: dup 4: invokespecial #3; //Method "<init>":()V 7: astore_1 8: new #4; //class Outer$Inner 11: dup 12: aload_1 13: dup 14: invokevirtual #5; //Method java/lang/Object.getClass:()Ljava/lang/Class; 17: pop 18: invokespecial #6; //Method Outer$Inner."<init>":(LOuter;)V 21: astore_2 22: return LineNumberTable: line 4: 0 line 5: 8 line 6: 22
其中,对应outer.new Inner()的部分是:
8: new #4; //class Outer$Inner 11: dup 12: aload_1 13: dup 14: invokevirtual #5; //Method java/lang/Object.getClass:()Ljava/lang/Class; 17: pop 18: invokespecial #6; //Method Outer$Inner."<init>":(LOuter;)V
可以看到里面有一处对outer.getClass()的调用,然而得到的结果却马上被pop指令抛弃掉了。
这个问题通过调试javac很容易解决。调试javac的方法可以参考以前一帖。
设置好调试环境后,在调试器里把上面的代码交给javac去编译。
简单猜测就可以知道,生成的对outer.getClass()方法的调用是在最终生成代码的时候才做的,而不是在更早阶段被解除的语法糖,所以我们要注意的目标就是com.sun.tools.javac.jvm.Gen类,其中的visitNewClass(JCNewClass tree)方法。
在这个方法设上断点,然后开始调试。
第一次碰到断点会是main()方法里的new Outer(),这个跳过。
然后第二次进来的时候,观察调试器的变量窗口,可以看到:

从javac的角度来看,源码里的
outer.new Inner()
被改写成了这种形式:
new Outer$Inner(outer<*nullchk*>)
(内部类被改名和改写为顶层类、隐式的外部类参数改写为显式参数)
接下来,有趣的点就是那个<*nullchk*>注释。
传给Inner()构造器的实际参数并不是原本的outer局部变量,而是outer局部变量外加一个空指针检查——要的值还是outer的值,不过如果outer为null的话,这里要抛出NullPointerException。
从javac的角度看,直接读outer局部变量可以用一个JCTree.JCIdent节点来表示,而这里则多包装了一个tag为JCTree.NULLCHK的JCTree.JCUnary节点。
正是在生成这个outer<*nullchk*>节点的代码时,会执行到Gen.visitUnary()的下述部分:
public void visitUnary(JCUnary tree) { // ... Item od = genExpr(tree.arg, operator.type.getParameterTypes().head); switch (tree.tag) { // ... case JCTree.NULLCHK: result = od.load(); code.emitop0(dup); genNullCheck(tree.pos()); break; }}其中的genNullCheck()是:
/** Generate a null check from the object value at stack top. */private void genNullCheck(DiagnosticPosition pos) { callMethod(pos, syms.objectType, names.getClass, List.<Type>nil(), false); code.emitop0(pop);}也就是说那个对getClass()的调用只不过是借invokevirtual指令来帮忙做null检查而已。getClass()本身得到的值其实是没用到的。
这个行为在Java语言规范里有相应的规定。在Java语言规范第三版,
15.9.2 Determining Enclosing Instances
该小节规定了应该使用什么对象作为外部类的实例
15.9.3 Choosing the Constructor and its Arguments
该小节规定了外部类实例在参数列表中的位置
15.9.4 Run-time Evaluation of Class Instance Creation Expressions
该小节规定了上面提到的空指针检查的行为:
public static void main(java.lang.String[]); Code: Stack=3, Locals=2, Args_size=1 0: new #1; //class Outer 3: dup 4: invokespecial #13; //Method "<init>":()V 7: astore_1 8: new #14; //class Outer$Inner 11: aload_1 12: dup 13: invokevirtual #16; //Method java/lang/Object.getClass:()Ljava/lang/Class; 16: pop 17: invokespeci8al #20; //Method Outer$Inner."<init>":(LOuter;)V 20: return LineNumberTable: line 4: 0 line 5: 8 line 6: 20
这当然不是偶然,因为getClass()方法是在Object上声明的(因此所有对象上必然存在),而且是final的(保证了它有确定的行为),而且运行开销比较低。
同样是Object上声明的方法,toString()、hashCode()之类其实也可以用,但它们都不是final的,有潜在可能性会引发较大的运行开销;这么分析一圈下来,Object上最好用的就剩下getClass()了。