RTTI
Java 作为面向对象编程语言,运行时的类型信息(RTTI)可以解决很多面向对象编程时的问题。
例如现在有一个基类Shape
,子类Circle
、Square
和Triangle
均继承自基类Shape
,我们可以使用基类的引用来达到一些操作,这样的话如果有扩展新的子类,也不会存在任何问题。
这个例子中最大的问题就是为什么我们可以用基类的引用来操纵子类对象,JVM 是如何得知基类和子类的继承关系的呢?
实际上在运行时,不管是什么类都会有它自身的类型信息,JVM 会根据具体的类型信息来判断一些操作的可行性。相反如果我们要将一个基类表示的对象强制转换为子类,那么类型信息就是用于判断该对象是否能够转换成功的依据。
Class对象
Java 中的运行时类型信息都是以Class
对象的形式表示,这也正符合 Java 一切皆对象的说法。
如何理解Class
对象呢?在 Java 程序中,创建一个类,那么编译后就能得到这个类的Class
对象,有了Class
对象后,JVM 会使用特定的类加载器来加载这个类,并且在加载过程中会检验是否完整,确保不包含不良代码。
加载完毕后,类就被载入内存,然后我们就可以拿这个类来创建对象了。
class A {
static {
System.out.println("load A");
}
}
class B {
static {
System.out.println("load B");
}
}
class C {
static {
System.out.println("load C");
}
}
public class test {
public static void main(String [] args) {
new A();//创建A类的对象
System.out.println("after load A");
try {
Class.forName("B");
} catch(ClassNotFoundException e) {
System.out.println("class not found");
}
System.out.println("after load B");
new C();
}
}
/*output:
load A
after load A
load B
after load B
load C
*/
上述例子可以发现,类在第一时间并没有被加载,而是在需要时进行加载,其中我们通过Class
类自带的forName()
方法来得到对应类的Class
对象。
除此之外,Class
类还有很多常用的方法如下:
static Class<?> forName(String className)
返回与带有给定字符串名的类或接口相关联的 Class 对象。
T cast(Object obj)
将一个对象强制转换成此 Class 对象所表示的类或接口。
String getName()
以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。
String getSimpleName()
返回源代码中给出的底层类的简称。
String getCanonicalName()
返回 Java Language Specification 中所定义的底层类的规范化名称。
boolean isInterface()
判定指定的 Class 对象是否表示一个接口类型。
Class<?>[] getInterfaces()
确定此对象所表示的类或接口实现的接口。
Class<? super T> getSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class。
T newInstance()
创建此 Class 对象所表示的类的一个新实例。
类字面常量
除了使用Class
类中的方法来获取类的Class
对象外,Java 还提供了类字面量class
来得到类型对象,例如:
test.class//获取类 test 的 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引用
我们可以在表示Class
对象时利用泛型来加以限定:
Class<Integer> intClass = int.calss;
先看一个例子:
//在类的继承体系中我们可以将子类对象用父类来表示,如:
Number obj = new Integer();
//那么如果是类的Class对象呢?
Class<Number> c = int.class;//这是不行的!!!
这里需要重新强调的是,Number 类是 Integer 类的父类,但是 Number 类的 Class 对象却不是 int 的 Class 对象的父类,也就是即便两个类是继承关系,但他们的 Class 对象却是没有继承关系的。
那如何才能实现呢?
可以使用通配符,Java 泛型中?
可以表示任何事物
Class<?> intClass = int.class;
Class<? extends Number> c = int.class;//可以表示所有Number子类的Class对象
Class<? super Integer> c1 = Number.class;//可以表示所有Integer的父类的Class对象
类型检查
-
在运行时,将一个对象的类型转换为另一个对象时,传统的做法是由 RTTI 来确保转换的可行性,如果转换错误则会抛出一个 ClassCastException 异常。
-
我们知道了 Class 对象后,可以通过得到 Class 对象来进行转型判断。在Class类中,通过
isInstance()
或者isAssignableFrom()
来判断是否可以转型
boolean isInstance(Object obj)
判定指定的 Object 是否与此 Class 所表示的对象赋值兼容。
boolean isAssignableFrom(Class<?> cls)
判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。
- 最后我们也可以使用
instanceof
关键字来判断转型是否可以进行。
例如:
//判断对象obj是否为Number类型
if(obj instanceof Number) {
...
}