`
RednaxelaFX
  • 浏览: 3049402 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

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
  该小节规定了上面提到的空指针检查的行为:
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 null, a NullPointerException 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.


规范里明确了“要抛出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()了。
  • 大小: 41.9 KB
分享到:
评论

相关推荐

    经典的问题

    - 匿名内部类是在创建对象时直接定义的一个内部类,通常用于实现接口或继承类。 **10. This与this()的区别** - `this`: 表示当前对象的引用。 - `this()`/`super()`: 用于在构造函数中调用其他构造函数。 **11....

    2021秋招】Java 面试知识点【精华背诵版

    通过反射,你可以获取类的信息(如字段、方法等),并能够动态地创建对象和调用方法。虽然反射非常灵活,但它也存在一定的局限性和潜在风险,比如可能会破坏封装性和降低性能。 ### Class 类的作用与获取方式 ####...

    java复习资料

    13. **编译时多态和运行时多态**:多态性体现在方法重载和方法覆盖上,编译时多态通过方法签名确定,运行时多态通过对象实际类型决定。 14. **equals()方法**:默认比较对象引用,重写后可比较对象内容。 15. **...

    java笔记se02.docx

    - **重写示例**:当打印一个对象时,系统会默认调用`toString()`方法,未重写前输出的是内存地址,重写后可自定义输出内容。 - **`equals()`方法**: - **源码**:`public boolean equals(Object o) { return ...

    第一次总结慢慢学习中(关于Java)

    当开发者使用`javac`编译`.java`源文件时,会产生`.class`字节码文件,这些字节码可以在任何支持JVM的平台上运行,实现了"一次编写,到处运行"的理念。 JVM内部的运作机制包括类加载器(ClassLoader),它负责加载`...

    java学习心得

    通过编写简单的程序,调用`new A().getClass().getClassLoader().toString()`可以查看当前类的加载器,从而直观地了解类加载的路径。理解这一机制对于解决“找不到类文件”的错误至关重要。 再者,JDK(Java ...

    java 反射

    Java反射机制允许程序在运行时检查类的结构,包括类的属性、方法和构造器等信息,并能动态地创建对象和调用其方法。这种能力使得开发者能够在不预先知道具体类名的情况下,对类进行操作,增强了代码的通用性和可...

    谈谈Java中的反射机制

    了解了反射机制,我们就可以在运行时动态创建对象,调用私有方法,甚至修改私有字段,这在许多场景下都非常有用,比如序列化、框架开发、单元测试等。然而,反射也带来了一些负面影响,如性能损失、安全性问题和增加...

    JAVA面试使用的笔试题目

    String str="i"创建了一个字符串常量池中的对象,而String str=new String("i")则在堆中创建了一个新的对象。 9. **字符串反转**: 可以使用StringBuilder或StringBuffer的reverse()方法来反转字符串。 10. **...

    JAVA编程经验汇总.txt

    通过`new A().getClass().getClassLoader().toString()`可以获取当前类的加载器信息。如果返回`null`,表示该类是由Boot ClassLoader加载的。 #### 3. Java开发工具链简介 选择合适的开发工具可以极大地提高开发...

    超全Java面试题(精简版)持续更新….

    - `finalize`:Object类中的一个方法,当垃圾回收机制准备回收对象时,可能会调用此方法进行清理工作,但这不是保证的。 6. **静态与实例变量**:静态变量属于类,所有类实例共享,可通过类名直接访问;实例变量...

Global site tag (gtag.js) - Google Analytics