RTTI(Run-Time Type Identification) 运行时类型识别,最先出现在C++里,引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。
Java中的RRTI则是源于《Thinking in Java》一书,可以在程序运行时发现和使用类型信息。这使得我们从只能在编译期执行面向类型的操作中解脱出来。主要有两种方式:一是传统的RRTI,它假定我们在编译期已知道了所有类型(如new对象时必须定义好类型);但并不是所有的Class都能在编译时明确,因此在某些情况下需要在运行时再发现和确定类型信息(比如:基于构建编程),这就需要第二种方式:反射机制,它允许我们在运行时发现和使用类型的信息。
按字面意思,我们在某些时候需要知道类的信息并使用它。具体看下面这个例子,我们定义了一个基类Animal及通用行为eat(),派生出的具体类有Dog、Cat和Brid,见下图。
在面向对象编程中基本目的是:让代码只操纵对基类(这里是Animal)的引用。为了实现这点,通常我们在创建具体类的对象时,都将其向上转型为对应的父类,然后在其余的代码中都使用这个父类,这是为了方便以后扩展,例如你可以向下面这样编码:
abstract class Animal {
void eat() {
System.out.println("吃");
}
}
class Dog extends Animal {
void eat() {
System.out.println("狗吃骨头");
}
}
class Cat extends Animal {
void eat() {
System.out.println("猫吃小鱼");
}
}
class Brid extends Animal {
void eat() {
System.out.println("鸟吃虫子");
}
}
public class AnimalTest {
public static void main(String[] args) {
List aList = Arrays.asList(
new Dog(), new Cat(), new Brid()
);
for (Animal animal : aList) {
animal.eat();
}
}
}
Output:
狗吃骨头
猫吃小鱼
鸟吃虫子
在这个例子中,创建 Dog、Cat、Brid对象放入List
中时会向上转型,同时也丢失了它们的具体类型,对于List而言,它们都是Animal类的对象。这时,假如我们需要知道某个泛化引用的确切类型,该怎么办呢?例如,我们只有骨头来喂食动物,猫和鸟并不吃骨头,怎么才能知道这个Animal对象是狗呢?或者我们想要拍动物在天空的照片,这时必须筛选出鸟类,因为其它动物不会飞。
使用RTTI,通过它可以查询某个Animal引用所指向的对象的确切类型,然后选择你需要的或者剔除你不要的。
想要理解RTTI在Java中的工作原理,首先得知道类型信息在运行时是如何表示的。Java用Class类来表示运行时的类型信息,首先必须明确,Class类跟Java API中定义的String、Integer等类以及我们自己定义的类是一样的,是一个实实在在的类,只不过名字特殊点,在JDK的java.lang包中。
那么Class类到底有什么作用呢?是什么的抽象,其实例又表示什么呢?
对于我们自己定义的类,我们用类来抽象现实中的某些事物,比如我们定义名为Dog的类来抽象现实中的狗,然后可以实例化这个类,用这些实例来表示一条黑狗、一条黄狗、我家的狗、你家的狗等等。我们还用Cat类来抽象现实中的猫,用Brid类来抽象现实中的鸟。那么,Dog、Cat、Brid这三个类之间有没有共同特征了,可不可以对这三个类进行抽象呢?
当然可以,我们都知道所有的class都是Object的子类,都有类名,有hashcode,可以判断类型属于class、interface、enum还是annotation。另外可以定义一些方法,比如获取某个方法、获取类型名等等。这样就封装了一个表示类型的类 — Class,用来提取这些类的一些共同特征,表示对这些类(或接口)的抽象。而Dog、Cat、Brid这三个类就分别是Class类的对象。也就是说,每个类都有一个Class对象,即每当我们编写并且编译一个新类,就会产生一个对应的Class对象,被保存在一个同名.class文件(编译后的字节码文件)里。
下面我们来分析一下Class类的源码:
//前一个Class表示这是一个类的声明,第二个Class是类的名称,
表示这是一个泛型类,并实现了四种接口。
public final class Class implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
//定义了三个静态变量
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
//定义了一个名为registerNatives()的本地方法,并在静态块中调用:
private static native void registerNatives();
static {
registerNatives();
}
// 私有构造函数,只能由JVM调用,创建该类实例
private Class(ClassLoader loader) {
classLoader = loader;
}
/*如果Class对象是一个Java类,返回class full_classname,即class 包名.类名;
比如上面例子的List,返回的就是class java.util.List;
如果是接口,将class改成interface;
如果是void类型,则返回void;
如果是基本类型,返回基本类型。*/
public String toString() {
return (isInterface() ?"interface " : (isPrimitive() ? "" : "class")) + getName();
}
注意:Class类的构造器是private的,这意味着我们无法用new关键字得到一个Class对象。为了生成一个类的Class对象,必须通过运行Java虚拟机(JVM)中的类加载器子系统。
从上我们可以总结出:
那么JVM是如何加载这个类的?
当程序创建第一个对类的静态成员的引用时,JVM中的类加载器子系统会将类对应的Class对象加载到JVM中。这个证明构造器也是类的静态方法,尽管构造器前并没有用static关键字修饰。因此,当我们使用new操作符创建一个类的实例对象时,也会被当作对类的静态成员的引用。
可以看出,Java一门动态加载的语言,Java中的类在需要时才会被加载。也就是说,我们编写出的Java程序,在它们开始运行之前并非被完全加载到内存的,其各个部分是在需要时才加载。因此,在我们需要用到某个类时,类加载器首先检查这个类的Class对象是否已被加载,如果没有加载,默认的类加载器就会先根据类名查找.class文件。在这个类的字节码文件被加载时,它们要接受验证,以确保其没有被破坏、并且不包含不良Java代码(这是java众多安全检测机制中的一个),检测通过后Class对象就被载入内存了,可以被用来创建这个类的所有实例对象。下图表示了一个类加载的过程:
第一阶段(加载):类加载器根据类名找到此类的.Class文件,并把这个文件包含的字节码加载到内存中,生成Class对象。
第二阶段(链接):又分为三个步骤,分别是:
(1) 验证阶段:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(2) 准备阶段:正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
(3)解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程。
第三阶段(初始化):类中静态属性和初始化赋值,以及静态块的执行。
Java主要提供了三种方式来获取一个实例对象对应的Class对象:
这个方法是Class类的一个static成员方法。Class对象和其他对象一样,我们可以获取并操作它的引用,forName()就是取得Class对象的引用的一种方法,该方法允许我们无需通过持有该类的实例对象引用而去获取Class对象。
try {
//"com.yang"是包名
Class c1 = Class.forName("com.yang.Dog");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
注意:如果Class.forName()没有找到你要加载的类,会抛出ClassNotFoundException异常。因此,在调用forName()方法时,需要向上面一样,给出一个ClassNotFoundException异常捕获。
通过new一个对象,用这个对象调用getClass()方法来获取Class的引用。这个方法属于根类Object的一部分,将返回表示该对象类型的Class引用。
Dog dog = new Dog();
Class c2 = dog.getClass();
//字面常量的方式获取Class对象
Class c3 = Dog.class;
用类字面常量的方式来生成Class对象的引用,在编译时会受到检查(因此不需要置于try语句块中来捕获异常),相对前面两种方式显得更简单、更安全。
采用字面常量的方式不仅可以应用于普通的类,也可以应用在接口、数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,关于反射技术稍后会分析。另外,因为基本数据类型有着对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下:
Class对象 | TYPE字段 |
---|---|
boolean.class | Boolean.TYPE |
char.class | Character.TYPE |
byte.class | Byte.TYPE |
short.class | Short.TYPE |
int.class | Integer.TYPE |
long.class | Long.TYPE |
float.class | Float.TYPE |
double.class | Double.TYPE |
void.class | Void.TYPE |
一般建议使用.class的形式,这样可以保持与普通类一致。
上面我们分析了类加载的三个步骤,初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常熟静态域进行首次引用时才执行,而使用“.class”来创建Class对象时,触发的是加载阶段,并不会触发最后阶段类的初始化,下面引用《Java编程思想》中的例子来说明这点:
import java.util.*;
class Initable {
//静态成员常量,编译期就确定值
static final int staticFinal = 47;
//静态成员常量,运行期才确定值
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
//静态初始化块
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
//静态成员变量
static int staticNonFinal = 147;
//静态初始化块
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
//静态成员变量
static int staticNonFinal = 74;
//静态初始化块
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
//字面常量方法获取Class对象
Class initable = Initable.class;
System.out.println("After creating Initable ref");
//不触发类初始化
System.out.println(Initable.staticFinal);
//会触发类初始化
System.out.println(Initable.staticFinal2);
//会触发类初始化
System.out.println(Initable2.staticNonFinal);
//forName()方法获取Class对象
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
根据运行结果可以看出:
(类型转换和反射将后续更新)