谜题80:更深层的反射

下面这个程序通过打印一个由反射创建的对象来产生输出。那么它会打印出什么
呢?

public class Outer{
public static void main(String[] args) throws Exception{
new Outer().greetWorld();
}
private void greetWorld()throws Exception {
System.out.println( Inner.class.newInstance() );
}
public class Inner{
public String toString(){
return "Hello world";
}

}
}

  

这个程序看起来是最普通的 Hello World 程序的又一个特殊的变体。Outer 中的
main 方法创建了一个 Outer 实例,并且调用了它的 greetWorld 方法,该方法以
字符串形式打印了通过反射创建的一个新的 Inner 实例。Inner 的 toString 方
法总是返回标准的问候语,所以程序的输出应该与往常一样,是 Hello World。
如果你尝试运行这个程序,你会发现实际的输出比较长,而且更加令人迷惑:

Exception in thread "main" InstantiationException: Outer$Inner
at java.lang.Class.newInstance0(Class.java:335)
at java.lang.Class.newInstance(Class.java:303)
at Outer.greetWorld(Outer.java:7)
at Outer.main(Outer.java:3)

  


为什么会抛出这个异常呢?从 5.0 版本开始,关于 Class.newInstance 的文档叙
述道:如果那个 Class 对象“代表了一个抽象类(abstract class),一个接口
(interface),一个数组类(array class),一个原始类型(primitive type),
或者是空(void);或者这个类没有任何空的[也就是无参数的]构造器;或者实
例化由于某些其他原因而失败,那么它就会抛出异常”[JAVA-API]。这里出现的
问题满足上面的哪些条件呢?遗憾的是,异常信息没有提供任何提示。在这些条
件中,只有后 2 个有可能会满足:要么是 Outer.Inner 没有空的构造器,要么是
实例化由于“某些其它原因”而失败了。正如 Outer.Inner 这种情况,当一个类
没有任何显式的构造器时,Java 会自动地提供一个不带参数的公共的缺省构造
器[JLS 8.8.9],所以它应该是有一个空构造器的。不过,newInstance 方法调
用失败的原因还是因为 Outer.Inner 没有空构造器!
一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一
个参数,这个参数表示了它的直接外围实例(immediately enclosing instance)
[JLS 13.1]。当你在代码中任何可以让编译器找到合适的外围实例的地方去调用
构造器的时候,这个参数就会被隐式地传递进去。但是,上述的过程只适用于普
通的构造器调用,也就是不使用反射的情况。当你使用反射调用构造器时,这个
隐藏的参数就需要被显式地传递,这对于 Class.newInstance 方法是不可能做到
的。要传递这个隐藏参数的唯一办法就是使用
java.lang.reflect.Constructor。当对程序进行了这样的修改后,它就可以正
常的打印出 Hello World 了:

private void greetWorld() throws Exception{
Constructor c = Inner.class.getConstructor(Outer.class);
System.out.println(c.newInstance(Outer.this));
}

  


作为其他的选择,你可能观察到了,Inner 实例并不需要一个外围的 Outer 实例,
所以可以将 Inner 类型声明为静态的(static)。除非你确实是需要一个外围实

例,否则你应该优先使用静态成员类(static member class)而不是非静态成
员类[EJ Item 18]。下面这个简单的修改就可以订正这个程序:

public static class Inner{...}


Java 程序的反射模型和它的语言模型是不同的。反射操作处于虚拟机层次,暴
露了很多从 Java 程序到 class 文件的翻译细节。这些细节当中的一部分由 Java
的语言规范来管理,但是其余的部分可能会随着不同的具体实现而有所不同。在
Java 语言的早期版本中,从 Java 程序到 class 文件的映射是很直接的,但是随
着一些不能被虚拟机直接支持的高级语言特性的加入,如嵌套类(nested
class)、协变返回类型(covariant return types)、泛型(generics)和枚
举类型(enums),使得这种映射变得越来越复杂了。
考虑到从 Java 程序到 class 文件的映射的复杂度,请避免使用反射来实例化内
部类。更一般地讲,当我们在用高级语言特性定义的程序元素之上使用反射的时
候,一定要小心,从反射的视角观察程序可能不同与从代码的视角去观察它。请
避免依赖那些没有被语言规范所管理的翻译细节。对于平台的实现者来说,这里
的教训就是要再次重申,请提供清晰准确的诊断信息。

猜你喜欢

转载自www.cnblogs.com/yuyu666/p/9841026.html
80