第14章-类型的信息RTTI

以JDK6为例

介绍

RTTI运行时类型信息使得可以在程序运行时发现和使用类型信息。

Run-Time Type Information

使用方式

Java是如何在运行时识别对象和类的信息的,主要有两种方式(还有辅助的第三种方式,见下描述)

  1. “传统的”RTTI,它假定在编译时已经知道所有的类型,比如Shape s = (Shape)s1
  2. “反射”机制,它运行在运行时发现和使用类的信息,即使用Class.forName()
  3. 关键字instanceof,它返回一个bool值,它保持类型的概念,它指的是”你是这个类吗?或者你是这个类的派生类吗?”。而如果用==equals比较实际的Class对象,就没有考虑继承它或者是这个确切的类型,或者不是。

    工作原理

    RTTI主要用来运行时获取对象到底是什么具体的类型。
    RTTI运行的时候,识别一个对象的类型。(运行的时候获取对象确切的类型)
    要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的,这项工作是由称为Class对象的特殊对象完成的,它包含与类有关的信息。Java Class对象来执行其RTTI,使用类加载器的子系统实现。
    类是程序的重要组成部分,每个类都有一个Class对象,每当编写并编译一个新类就会产生一个Class对象,它被保存在一个同名的.class文件中。在运行时,生成这个类的对象时,运行这个程序的Java虚拟机(JVM)会确认这个类的Class对象是否已经加载,如果尚未加载,JVM就会根据类名查找.class文件,并将其载入,一旦这个类的Class对象被载入内存,它就被用来创建这个类的所有对象。
    在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用,获取方式有三种。

    第1种

    如果没有持有该类型的对象,则Class.forName()就是实现此功能的便捷途,因为它不需要对象信息。

    第2种

    如果已经拥有类型的对象,那就可以通过调用getClass()方法来获取Class引用,它将返回表示该对象的实际类型的Class引用。 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    package rtti;

    interface HasBatteries {
    }

    interface WaterProof {
    }

    interface Shoots {
    }

    class Toy {
    Toy() {
    }

    Toy(int i) {
    }
    }

    class FancyToy extends Toy implements HasBatteries, WaterProof, Shoots {
    FancyToy() {
    super(1);
    }
    }

    public class RTTITest {

    static void printInfo(Class cc) {
    System.out.println("Class name: " + cc.getName() + ", is interface? [" + cc.isInterface() + "]");
    System.out.println("Simple name: " + cc.getSimpleName());
    System.out.println("Canonical name: " + cc.getCanonicalName());
    }

    public static void main(String[] args) {
    Class c = null;
    try {
    c = Class.forName("rtti.FancyToy"); // 必须是全限定名(包名+类名)
    } catch (ClassNotFoundException e) {
    System.out.println("Can't find FancyToy");
    System.exit(1);
    }
    printInfo(c);

    for (Class face : c.getInterfaces()) {
    printInfo(face);
    }

    Class up = c.getSuperclass();
    Object obj = null;
    try {
    // Requires default constructor.
    obj = up.newInstance();
    } catch (InstantiationException e) {
    System.out.println("Can't Instantiate");
    System.exit(1);
    } catch (IllegalAccessException e) {
    System.out.println("Can't access");
    System.exit(1);
    }
    printInfo(obj.getClass());
    }

    }

第3种

Java还提供另一种方法来生成对Class对象的引用,即使用类字面常量。比如上面的就像这样:FancyToy.class来引用。
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中),并且它根除对forName()的引用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。

注意

当使用.class来创建对Class对象的引用时,不会自动地初始化该Class对象,初始化被延迟到对静态方法(构造器隐式的是静态的)或者非final静态域(注意final静态域常量值不会触发初始化类操作)进行首次引用时才执行。而使用Class.forName()时会自动的初始化类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package rtti;

import java.util.Random;

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) {
// Does not trigger initialization
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization
System.out.println(Initable.staticFinal);
// Does trigger initialization(rand() is static method)
System.out.println(Initable.staticFinal2);

// Does trigger initialization(not final)
System.out.println(Initable2.staticNonFinal);

try {
Class initable3 = Class.forName("rtti.Initable3");
} catch (ClassNotFoundException e) {
System.out.println("Can't find Initable3");
System.exit(1);
}
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}

输出结果:

1
2
3
4
5
6
7
8
9
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

初始化方法实现尽可能有效的”惰性”。从这个结果可以看出使用.class方式来获取类的引用不会引发类的初始化。但是,使用Class.forName()立刻就进行初始化,就像对initable3引用的例子。

  1. staticfinal的值是一个编译器常量,获取这个值是不需要对类进行初始化的。如staticFinal
  2. staticfinal不是一个编译常量,比如new Random(47),这个是一个构造方法,构造方法是隐试的静态方法,所以会对类进行初始化。
  3. finalstatic对象,在使用的时候,首先会对类进行初始化操作。如Initable2#staticNonFinal

    RTTI的限制?如何突破?一反射机制

    如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,也就是在编译时,编译器必须知道所有要通过RTTI来处理的类。(RTTI的一种方式)
    可以突破这个限制吗?是的,突破它的就是反射机制。(反射机制也是RTTI的一种方式)
    Class类与java.lang.reflect类库一起对反射的概念进行支持,该类库包含FieldMethod以及Constructor类(每个类都实现Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样就可以使用Constructor创建新的对象,用get()/set()读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()getMethods()getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

    RTTI与反射机制的区别

    当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样),在用它做其他事情之前必须先加载那个类的Class对象,因此,那个类的.class文件对于JVM来说必须是可获取的,要么在本地机器上,要么可以通过网络取得。
    RTTI与反射之间真正的区别只在于;对RTTI来说,编译器在编译时打开和检查.class文件(也就是可以用普通方法调用对象的所有方法);而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
    RTTI和反射机制都是运行时获取类的信息。只是一个是编译的时候检查class文件,一个是运行时检查class文件。

    类的生命周期

    在一个类编译完成之后,下一步就需要开始使用类,如果要使用一个类,肯定离不开JVM。创建Class对象引用时,不会自动的初始化该Class对象。在程序执行中,JVM通过装载链接初始化这3个步骤完成Class对象的初始化。

    装载

    通过类加载器执行的,加载器将.class文件的二进制文件装入JVM的方法区,并且在堆区创建描述这个类的java.lang.Class对象。 (并不代表初始化)

    链接

    把二进制数据组装为可以运行的状态,验证类中的字节码,为静态域分配存储空间,创建类对其他类的所有引用。
    链接分为校验,准备,解析这3个阶段:
  4. 校验:一般用来确认此二进制文件是否适合当前的JVM(版本)。
  5. 准备:为静态成员分配内存空间,并设置默认值。int=0,实际上还是分配内存。
  6. 解析(创建类对其他类的所有引用):转换常量池中的代码作为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)。

完成之后,类型也就完成初始化,初始化之后类的对象就可以正常使用,直到一个对象不再使用之后,将被垃圾回收。释放空间。当没有任何引用指向Class对象时就会被卸载,结束类的生命周期。

初始化

对父类初始化,对静态方法初始化,代码块初始化和变量初始化。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×