Diving into Java Type Information: RTTI and Reflection

Reprinted from  "Deep Java" Type Information: RTTI and Reflection

1. RTTI Run-Time Type Infomation Run-time type information

Why do you need RTTI?

The more excellent the object-oriented design is, the more emphasis is placed on high cohesion and low coupling, as the principle of dependency inversion says: "Whether it is a high-level module or a low-level module, it should be aimed at abstract programming".

Say we have an abstract parent class:

Shape draw()

Here are the three concrete classes:

Circle draw()
Square draw()
Triangle draw()

In some cases, we hold Shape, but not enough - because we want to do special handling for its concrete type, yet our design is completely abstract, so the concrete type cannot be judged in the current context.
Because of the existence of RTTI, we can achieve our goal without destroying the design.


Class class and Class object

In fact, each class holds Classa reference to the object of its corresponding class (the Objectone in the class getClass()lets us get to it), which contains class-related information.
It is very easy to notice that for each class, compiling the Java file will generate a binary .classfile, which stores Classthe information of the object corresponding to the class.

.classis the file used by the classloader

Java programs are not fully loaded before running, the parts are loaded when needed.

Preparing to use a class consists of three steps:

  1. load. Executed by the class loader, looking up the bytecode, creating an Classobject.
  2. Link. Validates the bytecode, allocates storage for static fields, and resolves all references created by this class to other classes (say the class holds the staticfield) if necessary.
  3. initialization. If the class has a superclass, initialize it, execute the static initializer[note] and static initializer block.

注:原文为static initializers,经查看Thinking in Java,其意应为静态域在定义处的初始化,如:
static Dog d = new Dog(0);

所有的类都是在对其第一次使用时,动态加载到JVM中去的。当程序创建第一个对类的静态成员的引用时,JVM会使用类加载器来根据类名查找同名的.class——一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。构造器也是类的静态方法,使用new操作符创建新对象会被当作对类的静态成员的引用。
注意特例:如果一个static final值是编译期常量,读取这个值不需要对类进行初始化。所以说对于不变常量,我们总是应该使用static final修饰。


Class.forName(String str)

Class类有一个很有用的静态方法forName(String str),可以让我们对于某个类不进行创建就得到它的Class对象的引用,例如这个样子:

try {
     Class toyClass = Class.forName("com.duanze.Toy"); // 注意必须使用全限定名
} catch (ClassNotFoundException e) {

}

然而,使用forName(String str)有一个副作用:如果Toy类没有被加载,调用它会触发Toy类的static子句(静态初始化块)。

与之相比,更好用的是类字面常量,像是这样:

Class toyClass = Toy.class;

支持编译时检查,所以不会抛出异常。使用类字面常量创建Class对象的引用与forName(String str)不同,不会触发Toy类的static子句(静态初始化块)。所以,更简单更安全更高效。
类字面常量支持类、接口、数组、基本数据类型。


×拓展×

Class.forName(String className)使用装载当前类的类装载器来装载指定类。因为class.forName(String className)方法内部调用了Class.forName(className, true, this.getClass().getClassLoader())方法,如你所见,第三个参数就是指定类装载器,显而易见,它指定的是装载当前类的类装载器的实例,也就是this.getClass().getClassLoader();

你可以选择手动指定装载器:

ClassLoader cl = new  ClassLoader();   
Class c1 = cl.loadClass(String className, boolean resolve );

更详细的参考


范化的Class引用

通过范型以及通配符,我们能对Class对象的引用进行类型限定,像是:

Class<Integer> intClass = int.class; // 注意右边是基本数据类型的类字面常量

这样做的好处是能让编译器进行额外的类型检查。
知道了这一点以后,我们可以把之前的例子改写一下:

Class toyClass = Toy.class;
Class<?> toyClass = Toy.class;

虽然这两句是等价的,但从可读性来说Class<?>要优于Class,这说明编程者并不是由于疏忽而选择了非具体版本,而是特意选择了非具体版本。


Class.newInstance()

既然拿到了包含着类信息的Class对象的引用,我们理应可以构造出一个类的实例。Class.newInstance()就是这样一个方法,比如:

// One
try {
    Class<?> toyClass = Class.forName("com.duanze.Toy"); 
    Object obj = toyClass.newInstance();
} catch (ClassNotFoundException e) {

}

// Two
Class<?> toyClass = Toy.class;
Object obj = toyClass.newInstance();

使用newInstance()创建的类,必须带有默认构造器。
由于toyClass仅仅只是一个Class对象引用,在编译期不具备更进一步的类型信息,所以你使用newInstance()时只会得到一个Object引用。如果你需要拿到确切类型,需要这样做:

Class<Toy> toyClass = Toy.class;
Toy obj = toyClass.newInstance();

但是,如果你遇到下面的情况,还是只能拿到Object引用:

Class<SubToy> subToyClass = SubToy.class;
Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父类Toy的Class对象引用
// This won't compile:
// Class<Toy> upClass = subToy.getSuperclass();
// Only produces Object:
Object obj = upClass.newInstance();

虽然从常理上来讲,编译器应该在编译期就能知道SubToy的超类是Toy,但实际上却并不支持这样写:

// This won't compile:
Class<Toy> upClass = subToy.getSuperclass();

而只能够接受:

Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父类Toy

这看上去有些奇怪,但现状就是如此,我们惟有接受。好在这并不是什么大问题,因为转型操作并不困难。


类型检查

在进行类型转换之前,可以使用instanceof关键字进行类型检查,像是:

if ( x instanceof Shape ) {
     Shape s = (Shape)x;
}

一般情况下instanceof已经够用,但有些时候你可能需要更动态的测试途径:Class.isInstance(Class clz):

Class<Shape> s = Shape.class;
s.isInstance(x);

可以看到,与instanceof相比,isInstance()的左右两边都是可变的,这一动态性有时可以让大量包裹在if else...中的instanceof缩减为一句。


2.反射

不知道你注意到了没有,以上使用的RTTI都具有一个共同的限制:在编译时,编译器必须知道所有要通过RTTI来处理的类。

但有的时候,你获取了一个对象引用,然而其对应的类并不在你的程序空间中,怎么办?(这种情况并不少见,比如说你从磁盘文件或者网络中获取了一串字串,并且被告知这一串字串代表了一个类,这个类在编译器为你的程序生成代码之后才会出现。)

Class类和java.lang.reflect类库一同对反射的概念提供了支持。反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

  • RTTI,编译器在编译时打开和检查.class文件
  • 反射,运行时打开和检查.class文件

明白了以上概念后,什么getFields(),getMethods(),getConstructors()之类的方法基本上全都可以望文生义了。

我们可以看一下Android开发中经常用的对于ActionBar,让Overflow中的选项显示图标这一效果是怎么做出来的:

/*
overflow中的Action按钮应不应该显示图标,
是由MenuBuilder这个类的setOptionalIconsVisible方法来决定的,
如果我们在overflow被展开的时候给这个方法传入true,
那么里面的每一个Action按钮对应的图标就都会显示出来了。
*/

@Override
public boolean onMenuOpened(int featureId, Menu menu) {
    if (featureId == Window.FEATURE_ACTION_BAR && menu != null) {
        if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
            try {
                // Boolean.TYPE 同 boolean.class
                Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); 
                // 通过setAccessible(true),确保可以调用方法——即使是private方法
                m.setAccessible(true);
                // 相当于:menu.setOptionalIconsVisible(true) 
                m.invoke(menu, true);
            } catch (Exception e) {
            }
        }
    }
    return super.onMenuOpened(featureId, menu);
}

×拓展:动态代理×

Java中对于反射的一处重要使用为动态代理,可以参考这篇IBM developerworks的文章


参考资料


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325535880&siteId=291194637