有人问下面这段代码里,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()的部分是:


[code] 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[/code]


可以看到里面有一处对outer.getClass()的调用,然而得到的结果却马上被pop指令抛弃掉了。



这个问题通过调试javac很容易解决。调试javac的方法可以参考[url=http://hllvm.group.iteye.com/group/topic/24553]以前一帖[/url]。



设置好调试环境后,在调试器里把上面的代码交给javac去编译。


简单猜测就可以知道,生成的对outer.getClass()方法的调用是在最终生成代码的时候才做的,而不是在更早阶段被解除的语法糖,所以我们要注意的目标就是com.sun.tools.javac.jvm.Gen类,其中的visitNewClass(JCNewClass tree)方法。



在这个方法设上断点,然后开始调试。


第一次碰到断点会是main()方法里的new Outer(),这个跳过。


然后第二次进来的时候,观察调试器的变量窗口,可以看到:


[img]http://dl.iteye.com/upload/attachment/498417/c862cbdd-e4b5-3a8b-a449-467ba3f68ab6.png[/img]


从javac的角度来看,源码里的


outer.new Inner()



被改写成了这种形式:


[code]new Outer$Inner(outer<*nullchk*>)[/code]


(内部类被改名和改写为顶层类、隐式的外部类参数改写为显式参数)



接下来,有趣的点就是那个<*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


该小节规定了上面提到的空指针检查的行为:


[quote="Java Language Specification, 3rd Edition"]At run time, evaluation of a class instance creation expression is as follows.


First, if the class instance creation expression is a qualified class instance creation expression, the qualifying primary expression is evaluated. If the qualifying expression evaluates to [b]null[/b], a [b]NullPointerException[/b] is raised, and the class instance creation expression completes abruptly. If the qualifying expression completes abruptly, the class instance creation expression completes abruptly for the same reason.[/quote]



规范里明确了“要抛出NullPointerException”的行为,至于是如何实现null检查的则没规定,可以由实现自由发挥。用普通的if...else来做这个检查当然也可以,只不过生成的字节码就比调用getClass()的办法更长一些。



====================================================================



无独有偶,ECJ(Eclipse Compiler for Java)在编译这种代码的时候同样会生成对getClass()方法的调用:


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