Java类型信息

RTTI(Run-Time Type Identification运行时类型识别)

​ 运行时类型信息让我们可以在程序运行的过程中发现和使用类型信息

​ 含义:在运行时,识别一个对象的类型(所有的类型转换都是在运行时进行正确性检查的)

两种方式识别对象和类的信息
  1. 传统的RTTI
  2. 反射机制
为什么需要RTTI

​ 先来了解一下多态的概念

多态

​ 面向对象编程基本的目的是:让代码只操作对基类的引用,这样如果要添加一个新类来扩展程序就不会影响到原来的代码。所以即使是通过泛化的对象引用来调用,也能产生正确的行为。这就是多态。典型的例子:Shape基类有多个子类Circle、Square等

1
2
3
4
List<Shape> shapeList = Arrays.asList(new Circle(), new Square());
for(Shape shape: shapeList) {
shape.draw();
}

​ 在上面的代码中,Circle、Square会发生向上转型,在转型的过程中也失去了具体类型信息。对于shapeList来说,他们只是Shape对象。

​ 而当从容器中取出对象的时候,实际上他们都会被当做Object持有,只不过会自动的将结果转型Shape,这里的RTTI类型转换并不彻底,Object只转型成了Shape但是没有转型为其子类,因为在List中,我们只知道保存的都是Shape,在编译时通过java的容器和泛型来强制确保这一点,而在运行时由类型转换来确保这一点。

​ 而接下去的事情就要交给多态了

class对象

​ Class对象给出了类型信息在运行时是如何表示的,它包含了与类的有关信息,用来创建类的所有“常规”对象,被保存在与类名同名的一个.class文件中。Java使用Class对象来执行其RTTI

​ 所有的类都是在对其第一次引用时,动态的加载到JVM中的

​ 所以,Java程序在它开始运行之前并非完全被加载,而是在需要时动态的加载到JVM中的,而一旦一个类的Class对象被加载到内存中之后,它就会被用来创建这个类的所有对象

获取Class对象

获取Class对象的几种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. Class.forName("XXX");
/*
a. 使用这种方式获取Class对象会导致该类被加载,前提是该类还没有被加载的情况下,使用这种方式的好处是可以不用通过对象实例来获取Class对象;
b. 但是使用这种方式必须要捕获ClassNotFoundException异常,因为在编译期forName()无法对传入的字符串进行检查对应的类是否真实存在的,只能在运行时进行检查,如果不存在,则抛出异常
*/

2. Student.class;
/*
a. 更简便,且更安全,因为它在编译时就会受到检查(所以也不需要置于try语句块中),根除了forName()方法的调用,也更高效了,所以建议使用这种方式来获取;
b. 这种方式不仅可以应用于类,也可以应用于接口、数组以及基本数据类型;
c. 这种方式触发的是类的加载阶段,在这个阶段类的创建已经完成,获取其引用并不困难,但是不会触发类的初始化过程

*/
3. obj.getClass();
/*
也会触发类的初始化过程
*/

来看个例子:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.cjh.study.classinfo;

import java.util.Random;

/**
* @ClassName InitTest
* @Description TODO
* @Author cjh
* @Date 2019/5/4 16:18
* @Version 1.0
*/
public class ClassInitialization {

public static int random = new Random().nextInt(100);

public static void main(String[] args) {
// 使用字面量方式获取Class对象
Class<InitTest> initTestClass = InitTest.class;

System.out.println("创建InitTest引用...");

// 不会触发类的初始化
System.out.println("InitTest compileStaticFinal = " +
InitTest.compileStaticFinal);

// 会触发类的初始化
System.out.println("InitTest nonCompileStaticFinal= " +
InitTest.nonCompileStaticFinal);

// 会触发类的初始化
System.out.println("InitTest2.staticNonFinal = " +
InitTest2.staticNonFinal);

try {
Class<?> initTest3 =
Class.forName("com.cjh.study.classinfo.InitTest3");

System.out.println("创建InitTest3引用...");

// 会触发类的初始化
System.out.println("InitTest3.staticNonFinal = " +
InitTest3.staticNonFinal);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}

class InitTest {

// 编译期静态常量
static final int compileStaticFinal = 12;

// 非编译期静态常量
static final int nonCompileStaticFinal = ClassInitialization.random ;

static {
System.out.println("初始化 InitTest...");
}

}

class InitTest2 {
// 静态成员变量
static int staticNonFinal = 23;

static {
System.out.println("初始化 InitTest2...");
}
}

class InitTest3 {
// 静态成员变量
static int staticNonFinal = 55;

static {
System.out.println("初始化 InitTest3...");
}
}

输出结果:
创建InitTest引用...
InitTest compileStaticFinal = 12
初始化 InitTest...
InitTest nonCompileStaticFinal = 32
初始化 InitTest2...
InitTest2.staticNonFinal = 23
初始化 InitTest3...
创建InitTest3引用...
InitTest3.staticNonFinal = 55

​ 从如上的代码结果可以得出结论:

1. 使用字面量的方式获取Class对象不会触发类的初始化
 2. 访问编译期静态常量(如:InitTest.compileStaticFinal)不会触发类的初始化。这是因为<font color=red>编译期静态常量会通过传播优化的方式被存储到一个NotInitialization常量池中</font>,如InitTest中的静态常量会被存储到NotInitialization常量池中,而之后对InitTest的常量的引用就会被转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期静态常量的获取都是从NotInitialization池中获取的,所以这是编译期静态常量不触发类的初始化的重要原因。
泛化的Class引用

类型转换

​ 许多需要类型转换的场景中,我们更多的是进行的强制类型转换,例如:

1
2
Animal ani = new Dog();
Dog dog = (Dog) ani;

​ 之所以可以进行强制类型转换,这完全要归功于RTTI,所有的类型转换都是在运行时进行正确性检查的,利用RTTI验证类型是否正确从而确保强制类型转换的完成,如果类型转换失败,那么将会抛出类型转换异常。

instanceof(RTTI的第三种形式)

​ 在进行向下的类型转换时,可以通过判断 x instanceof Animal的返回值来判定是否可以进行向下的类型转换操作。

​ 它的返回结果和isInstanceOf( )方法一致,如:class.isInstanceOf( obj ); isInstanceOf()是Class类Native方法,其中obj是被测试的对象或者变量,如果obj是调用这个方法的class或者接口的实例,则返回true