首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

javac在编译创造内部类对象时生成的奇怪的getClass()调用是什么

2012-11-08 
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()了。

热点排行