运行时识别对象和类的信息主要有两种方式:一种是“传统的”RTTI,它默认我们在编译时已经知道了所有的类型;一种是反射,它允许我们在运行时发现和使用类的信息。
今天首先来认识下RTTI。
1.什么是RTTI以及为什么需要RTTI
RTTI的英文全称是Run-Time Type Identification,即运行时类型识别。它可以在程序运行时检查父类型的引用是否可以指向子类型的对象,即确保类型向上转换安全。
示例1:
abstract class Shape {
void draw() {
System.out.println(this + ".draw()");
}
abstract public String toString();
}
class Circle extends Shape {
public String toString() { return "Circle";}
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
for (Shape shape : shapeList) {
shape.draw();
}
}
}
运行结果:
Circle.draw()
Square.draw()
Triangle.draw()
在Java中,所有的类型转换都是在运行时进行正确性检查的。对于编译器来讲,它只知道容器类放入的是Shape父类引用,而不知引用指向的具体对象是什么对象,那么这个信息的确认就可以通过RTTI来保证,即找到Shape父类引用指向的对象类型。
那么RTTI识别父类引用指向的对象类型这个功能有什么用呢?
假设我们需要对Square类和Triangle类做其他操作,而当识别到Circle类时就跳过,那么识别对象类型就非常有必要了。
2.既然RTTI的主要作用是在运行时识别类型信息,那么类型信息在运行时是如何表示的呢?
我们知道编译器会根据Java代码构建抽象语法树,然后编译生成字节码,表现出来就是一个十六进制的.class文件;之后,随着程序启动JVM也跟着启动,在需要类的时候加载.class文件到JVM内存进行一系列的工作。这里一个.class文件其实就是一个类,那么要想弄明白类型信息在运行时是如何表示的,就需要弄明白.class文件加载到JVM内存之后是如何表示的。
在这里,JVM会把.class文件当作一个Class类的对象,JVM需要使用某个类的时候,首先由类加载器加载该类的Class对象,即.class文件,然后由该Class对象完成类对象的构造。下面用例子证明类的Class对象被载入JVM内存后,它就被用来创建这个类的所有对象:示例2
class A {
static { System.out.println("A static");}
}
class B {
static { System.out.println("B static");}
}
class C {
static { System.out.println("C static");}
}
public class Test1 {
public static void main(String[] args) {
new A();
try {
Class<?> cs = Class.forName("com.starry.rtti.B"); // B类的全路径类名
System.out.println(cs.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
new C();
}
}
这里需要解释下这行语句:
Class<?> cs = Class.forName("com.starry.rtti.B");
之前说过,每个类都有一个Class对象,而forName(arg)方法是取得arg类的Class对象的引用;示例2中是获取B类的Class对象的引用;这里的输出结果是:
A static
B static
class com.starry.rtti.B
C static
结果表明:①forName()方法的调用会使得类B加载到JVM内存;②返回B类的Class对象的引用,并打印引用,即class和引用的名字;(感兴趣的可以查看Class类的toString()方法源码)
这里Class对象的引用cs非常重要,它是你在运行时时使用类型信息的唯一途径。当然,获取类B的Class对象的引用还有一种方式,即通过B类对象的方法getClass():示例3
A a = new A();
Class<?> cs1 = a.getClass();
注意:getClass()方法是Object类的一个本地方法。
Java里面还提供一种方法来获取类的Class对象的引用:类字面常量。
Class<?> cs2 = A.class;
System.out.println(cs2.toString());
这种方式非常简单高效,不需要对象,也不需要捕获异常,所以比较推荐使用这种方式。但是如果你实际操作过的话,你会发现这种方式并不能自动化地初始化该Class对象。下面写个示例来证明下:示例4
class Initable {
static final int staticFinal = 10;
static final int staticFinal2 = ClassInitialization.random.nextInt(100);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 100;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 11;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random random = new Random(100);
public static void main(String[] args) throws Exception {
Class<?> initable = Initable.class;
System.out.println("创建Initable类的class对象引用后...");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFinal2);
System.out.println("======================");
System.out.println(Initable2.staticNonFinal);
System.out.println("======================");
Class<?> initable3 = Class.forName("com.starry.rtti.Initable3");
System.out.println("创建Initable3类的class对象引用后....");
System.out.println(Initable3.staticNonFinal);
}
}
运行结果:
创建Initable类的class对象引用后...
10
Initializing Initable
15
======================
Initializing Initable2
100
======================
Initializing Initable3
创建Initable3类的class对象引用后....
11
结果进一步表明:通过Initable.class获取Class对象的引用并不能初始化Initable类;如果不能理解第二行和第三行的输出结果,可以看这篇文章。
上面介绍了获取Class对象引用的三种方法,即识别运行时类的信息:
- Class.forName(arg)方法;
- 对象的.getClass()方法;
- 类字面常量;
获取到Class对象的引用,就能在运行时获取类性信息。多态中,父类的引用可以指向子类的对象,运行过程中,子类对象的Class对象包含父类的信息,因此可以安全的向上转型为父类的引用。