附上思维导图。这篇博客虽然不是完全按照这份思维导图写的,但是主要讲了以下的知识点。

java 类信息存放在哪 java运行时类型信息_java


在《Thinking in Java》的第十四章类型信息中,提到了运行时类型鉴别(Run-Time Type Identification, RTTI)。其实RTTI的说法是源自于C++的,在C++中通过RTTI可以得到基类指针或引用所指对象的实际类型。

在Java中,有叫做反射(Reflection)的机制。它不仅可以完成运行时类型鉴别,还可以在运行时获得对象和类型的信息,而且还能动态加载类,动态访问以及调用目标类型的字段,方法以及构造函数。

看书的时候总是给这俩名词给搞混掉。但其实反射和RTTI就是Java和C++在同一件事情上的两种描述而已。只是《Think in Java》的作者先学的C++,在写书的时候把RTTI给搬进来了,书中的传统的RTTI指的其实就是C++中的RTTI吧。

看了前面三段话,大家可能会想,啥是RTTI啊,有啥用啊,动态加载类又是个啥玩意……。之后我们会从为什么要有RTTI开始,一点一点讲这些个知识点。

为什么需要RTTI

个人观点,RTTI是为了补偿多态机制而存在的。多态机制要求我们尽量编写家族通用的代码。至于为什么要编写家族通用的代码,请见下一段。

假设我们有一个Animal家族,这个家族里有Dog类Cat类Mouse类等等的各种各样的动物类,这些类都继承自Animal类,从伦理上来讲,这些个Dog啊,Cat啊,同时也是AnimalAnimal能有的行为(方法),它们也都有。为了编写家族通用的代码,我们总是用Animal作为这个家族的代表,将其他对象交给Animal引用,然后通过Animal引用来访问这些个对象。下面见一段跑不通的代码。

public class AnimalRestaurant{

    public static void serveAnimal(Animal hungryAnimal){
        ...各种准备工作...

        // 由于多态机制,会根据hungryAnimal引用的对象类型,调用相应的eat(food)方法
        dirtyAnimal.eat(food); 

        ...付钱走人...
    }

}

在上面这一段代码中,我们开了一家动物餐厅,专门为动物服务。按上面这种写法,那我们这个世界的动物要去餐厅吃饭时,不管是什么动物,只要调用AnimalRestaurant.serveAnimal(Animal hungryAnimal)这个方法就行了。而不用为每个动物都写一个方法,像什么serveDogserveCat都不需要。所以代码的重用性就很高,一份代码家族里的所有类都能用。而且代码的耦合度很低,因为动物的eat行为是动物自己定的,不是我们写死的。所以代码也易扩展,有新动物来了,只要多加一个动物类就好了,这份代码根本不用动,因为是家族通用的。……

其实更专业一点地讲,多态要我们编写泛化的代码。代码要尽量少地关心对象的具体类型,所以在编写代码的时候,我们主动丢失了对象的类型信息。但有时我们就是需要知道对象的具体类型,比如上面动物餐厅例子中,我们可能需要统计今天到店里来的每种动物的数量,来决定之后的经营方针。所以我们需要RTTI,假如没有多态,那么这个对象的类型就一目了然了,也就美RTTI的事了。

RTTI在Java中的具体应用

Java中用到RTTI的地方有这么几个。

  1. 向下转型时的类型安全检查。在将基类引用指向的对象转型为子类型,Java会获取这个对象的具体类型,并检查这个转型是否正确,如果不正确,就抛出一个ClassCastException异常。
  2. 关键字instanceof。通过instanceof关键字,我们可以判断一个对象是否是某个类型的实例。如x instanceof Dog,就可以判断x指向的对象是否是一个Dog类的实例。
  3. Reflection API。通过Reflection API,我们可以在运行时得到类型和对象的类型信息,而且还能动态加载类,动态访问类型的方法,字段。
  4. 上面说的都是Java语言提供给编程者使用的RTTI功能。如果把RTTI作为一种在运行时获取类型信息的思想,那么多态机制也是通过RTTI实现的,在运行时根据对象的具体类型调用相应的方法。

接下来,我们主要会讲Reflection API。前两项都比较简单,就略过了。

反射

通过反射我们可以在运行时获取类型信息,像是类的名字,方法,字段,此外还可以用反射来创建一个新对象,访问其中的方法,字段。Java的反射特殊在可以在编译时不知道这个类的情况下来使用这个类。

在Reflect API中,有几个主要的类。

  1. Class类。对应类。
  2. Constructor类。对应类的构造方法。
  3. Method类。对应类中的方法。
  4. Field类。对应类中的字段。
  5. Annotation类。对应注解。
  6. Array类。对应数组。

关于反射的使用可以在如下网址中学习,思路很干净很清晰。在这里只介绍几个知识点。

运行时类型信息的表示

一个类的类型信息在运行时,可以通过它的Class对象来访问。什么意思呢?就是说你写了一个类,里面有各种方法啊字段啊,在运行时,这个类的这些个方法啊字段啊,以及类的名字啊,它爸是谁啊什么的这些类型信息都可以通过这个类的Class对象访问。

每一个类都有一个Class对象,注意类的Class对象不是该类的实例对象哇。每当编写并且编译了一个新类,就会生成一个同名的.class文件,其中保存了一个类的类型信息。而当Java程序运行需要某个类时,JVM会通过被称为类加载器的子系统从.class文件中载入类型信息,并在堆中新建一个该类的Class对象,通过这个对象可以获取这个类的类型信息。

Java代码的动态加载机制

所有的类都是在对其第一次使用时,动态加载到JVM(Java Virtual Machine)中的。注意,这是一个很有逼格的小知识点!

我们从程序的启动开始。我们先编写了如下的一个Launcher类,它的main函数里面还使用了一个Rocket类。

public class Launcher {
        public static void main(String[] args) {
            System.out.println("Program start!");
            Rocket rocket = new Rocket();
            ...
        }       }

然后我们以指令javac Launcher.java进行编译,生成一个.class文件。之后,就通过指令java Launcher尝试调用Launcher类的main方法,执行时,JVM发现需要用到Launcher类,所以它通过读取.class文件加载了Launcher类,并在堆中生成了Class对象,然后才找到了它的main方法开始了程序。

这个时候,Rocket并没有被加载到JVM中,直到执行到new Rocket,JVM才去加载它。所以说Java的代码是动态加载的。Java的这种机制可以实现一些很炫酷的功能,像Android中的插件化,其实就是利用了java代码动态加载的特点,实现了类似在汽车跑起来的时候给他换轮胎这样的不可思议功能,又比如分布式计算啊,IDE的可视化编程方法都需要在运行时加入编译时未知的类,这些编译时未知的类可以用反射在运行时获取她们的类信息,并调用这些类的实例对象的方法!

动态加载是Java的一大特点,像C++的代码,是在运行前就全部加载好的。你可能会问C++为什么不支持动态加载呢?因为C++不像Java,它没有JVM来中转这些代码。私以为Java和C++最大的区别就是Java多了一个虚拟机。

Got it?

两个RTTI

个人觉得应该有两个RTTI。一个是C++中的运行时类型鉴别(Run-Time Type Identification, RTTI)。另一个是面向对象编程(Object Oriented Programming, OOP)语言中都会有的在运行时获取类型信息的功能,运行时类型信息(Run-Time Type Information)。后者是一个抽象类,前者是C++对这个类的一个实现。