简介
运行时类型信息使得我们可以在程序运行时发现和使用类型
它使得我们从只能在编译期执行面向类型的操作和禁锢中解脱出来
有人会对标题上的RTTI感觉到陌生,RTTI的全名时Run-Time Type Identification,表示运行时类型识别。但是学过C++或者看过Thinking in Java的同学们都应该对此有些熟悉,我在看Thinking in Java这本书时,其实抱有很大的疑惑,因为我没有办法很清晰地清淅的区分出RTTI和反射的概念,在区分两者上,总是会存在一些疑惑。但是后来查阅各方资料,发现两者其实是同一种东西,也就是说RTTI与反射其实就是在不同的维度上去描述同一个事物,它们都是在运行时获取到对象的类型信息并根据获得的类型信息进行操作。由于Thinking in Java的作者本身在写Thinking in Java之前写了一本Thinking in C++的,所以在写这本书时也沿用了C++中RTTI的思想去分析问题。所以才造成这个情况。在本章,我们将RTTI和反射作为一个事物去理解
为什么需要运行时获取类型信息
我们看一个多态的例子
这是一个典型的类层次结构图。面向对象编程的基本目的就是:让代码只操纵对基类(Shape)的引用。这样如果要添加一个新类来拓展程序,就不会影响到原来的代码。这个例子中Shape接口动态绑定了draw()方法,目的就是为了让程序员使用泛化的Shape引用来调用draw()。由于draw会在导出类中被覆盖并且因为是动态绑定的,所以即时是通过泛化的Shape引用来调用,也能产生正确的行为。这就是多态。
public class demo1 {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(new Circle(),new Square(),new Tranigle());
for (Shape shape: shapeList) {
shape.draw();
}
}
}
abstract class Shape{
void draw(){
System.out.println(this+".draw()");
};
public abstract String toString();
}
class Circle extends Shape{
@Override
public String toString() {
return "Circle";
}
}
class Square extends Shape{
@Override
public String toString() {
return "Square";
}
}
class Tranigle extends Shape{
@Override
public String toString() {
return "Tranigle";
}
}
在这个例子中,当把Shpe对象放入List<Shape>时会向上转型,但在向上转型为Shape的时候也就丢失了Shape对象的具体类型。对于数组而言,它们都只是Shape类的对象。
当从数组中取出元素时,这种容器——实际上它将所有的事物都当作Object持有,这有关泛型的类型擦除,我们讲解泛型的文章会提到——会自动将结果转型回Shape。这是RTTI的基本使用方式,这种类型转换会在程序运行时进行类型转换的正确性检查。而这种正确性检查正是依赖于运行时获取类型信息的能力
在Java中,所有的类型转换都是在运行时进行正确性检查的。
在上面例子中这种转型并不彻底:Object被转型为Shape,而不是转型为Circle或是其他实际的类型。因为我们只知道这个列表中保存的都是Shape。在编译时,将由容器和java的泛型机制来确保这点,而在运行时,由RTTI来确保这点。
接下来就是多态机制的事情了,Shape对象具体要执行什么代码是由引用所指向的具体对象的实际类型去决定的。
Class对象
要理解运行时类型识别的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。Java使用Class对象来实现类型识别,即使你在执行的是类似转型这样的操作。
类是程序的一部分,每一个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的JVM将使用类加载器系统
类加载器系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是所谓的可信类,包括Java API类,它们通常是从本地磁盘加载的。在这条链中,通常不需要添加额外的类加载器,但如果存在特殊需求(例如以某种特殊方式加载类,以支持Web服务器应用),那么你有一种方式可以挂接额外的类加载器。
所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。
因此Java在开始运行前并不会去加载类,其各个部分是在必须时才加载的。这在像C++这样静态加载语言中是很难做到的。
类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并不包含不良Java代码。
一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象
public class RTTILearning2 {
public static void main(String[] args) {
new Candy1();
Candy2.f();
/*loading Candy1
loading Candy2*/
}
}
class Candy1{
static {
System.out.println("loading Candy1");
}
}
class Candy2{
static {
System.out.println("loading Candy2");
}
public static void f() { }
}
上面代码中我们发现在new和调用静态方法时,都会触发目标类的静态初始化,而静态初始化只会发生在类对象被加载时。
获取类对象
我们前面提到,每一个类都会有一个类对象,那么这个类对象该如何获取呢
package org.example.RTTILearning;
public class RTTILearning3 {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz1 = Class.forName("org.example.RTTILearning.Gum1");
Class clazz2 = Gum2.class;
Class clazz3 = new Gum3().getClass();
/*loading Gum1
loading Gum3*/
}
}
class Gum1{
static {
System.out.println("loading Gum1");
}
}
class Gum2{
static {
System.out.println("loading Gum2");
}
}
class Gum3{
static {
System.out.println("loading Gum3");
}
}
上面代码中我们使用了三种方式去获取了类对象,第一种时通过Class.forName(),其参数必须是我们要加载的类的全限定类名,也就是要加上包前缀,否则将提示ClassNotFoundException异常。第二种是直接通过类名.class获取类对象,但是根据输出结果我们发现这种方式并没有触发这个类的静态初始化(关于这个原因我们后面再介绍,但是我们必须记住这种方式不会引起静态初始化)。第三种方式是通过实例来获取类对象。
无论何时,只要你想再运行时使用类型信息,就必须首先对恰当的Class对象的引用。
使用类字面量获取类对象为什么不会引起静态初始化
为了解释使用类字面量获取类对象(类名.class)为什么不会引起静态初始化,我们这里简单提一下虚拟机加载类的过程。为了使用类而作的准备工作实际包含三个步骤:
- 加载,由类加载器执行。该步骤将查找字节码,并从字节码中创建一个Class对象。
- 链接,在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果有必要,会解析这个类创建的对其他类的所有引用
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
而通过类字面量获取类对象不会引起第三步的初始化。
如果我们访问的是类中的static final的值,也会有一些不一样的发现。如果这个值是一个编译器常量,那么这个引用会在编译期就被直接用常量替换掉。而这样的静态常量会被直接存放在常量池中,而不是类对象的静态存储空间中,所以对static fianl修饰的常量引用甚至都不会引起链接和初始化两个步骤。
反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。事实上,反射就是通过我们前面提到的类对象中包含的类型信息来获知这些方法和属性的
Class类和java.lang.reflect类库一起对反射的概念进行了支持。该类库包含了Field、Method以及Constructor类(每一个都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用于表示未知类里对应的成员。这样我们就可以使用Constructor实例化新对象,使用get/set方法读取和修改Field对象关联的字段,用invoke方法调用和Method对象关联的方法。另外还可以调用getFields()等便利的方法,以返回表示字段、方法以及构造器的对象的数字。这样匿名对象的类信息就能在运行时被完全确定下来,而在编译器不需要知道任何事情。
反射相关的类
类名 | 用途 |
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量(成员变量也称为类的属性) |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造方法 |
类对象的基本操作
前面提到可以从类对象中获得类型信息,所以下面就是一些获取类型信息的方法
方法 | 介绍 |
getName() | 获取类的全限定类名 |
getInterfaces() | 返回一个类所实现的接口的类对象数组 |
getSimpleName() | 获取类名 |
getSuperclass() | 获取父类的类对象 |
getInterfaces() | 获得当前类实现的类或是接口 |
newInstance() | 调用类对象对应的无参构造函数来实例化一个对象,返回的是一个Object引用 |
cast() | 强制类型转换 |
isAssignableFrom() | 是否派生自目标类型 |
isInstance() | 类与参数对象是否匹配 |
getClassLoader() | 获取类的类加载器 |
getClasses() | 返回类中定义的公共、私有、保护的内部类(包含父类中的内部类) |
getDeclaredClasses() | 返回类中定义的公共、私有、保护的内部类(不包含父类中的内部类) |
getPackage() | 返回包名 |
属性相关的方法
方法 | 介绍 |
getField(String name) | 获得某个public属性对象(包含父类中的) |
getFields() | 获得所有的public属性对象(包含父类中的) |
getDeclaredField(String name) | 获得某个public属性对象(不包含父类中的) |
getDeclaredFields() | 获得所有的public属性对象(不包含父类中的) |
注解相关的方法
方法 | 介绍 |
getAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的公有注解对象 |
getAnnotations() | 返回该类所有的公有注解对象 |
getDeclaredAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的所有注解对象 |
getDeclaredAnnotations() | 返回该类所有的注解对象 |
方法相关的方法
方法 | 介绍 |
getMethod(String name, Class…<?> parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class…<?> parameterTypes) | 获得该类某个方法 |
getDeclaredMethods() | 获得该类所有方法 |
其他重要的方法
方法 | 介绍 |
isAnnotation() | 如果是注解类型则返回true |
isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果是指定类型注解类型则返回true |
isAnonymousClass() | 如果是匿名类则返回true |
isArray() | 如果是一个数组类则返回true |
isEnum() | 如果是枚举类则返回true |
isInstance(Object obj) | 如果obj是该类的实例则返回true |
isInterface() | 如果是接口类则返回true |
isLocalClass() | 如果是局部类则返回true |
isMemberClass() | 如果是内部类则返回true |
Field类
方法 | 介绍 |
equals(Object obj) | 属性与obj相等则返回true |
get(Object obj) | 获得obj中对应的属性值 |
set(Object obj, Object value) | 设置obj中对应属性值 |
Method类
方法 | 介绍 |
invoke(Object obj, Object… args) | 传递object对象及参数调用该对象对应的方法 |
Constructor类
方法 | 介绍 |
newInstance(Object… initargs) 根据传递的参数创建类的对象
instanceof
关键字instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
if(x instanceof Dog){(Dog)x.bark();}
instanceof与Class的等价性
在查询类型信息时,以instanceof与直接比较Class对象有一个很重要的差别。
public class RTTILearning4 {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Benz();
System.out.println(car1.getClass().isInstance(car2));//true
System.out.println(car2 instanceof Car);//true
System.out.println(car2.getClass() == car1.getClass());//true
System.out.println(car1.getClass().isInstance(car3));//true
System.out.println(car3 instanceof Car);//true
System.out.println(car3.getClass() == car1.getClass());//false
}
}
class Car{}
class Benz extends Car{}
通过上面代码我们发现instanceof和isInstance()在上下对比中都保持了一致,也就是说,两者都是判断目标对象是不是该类型或者该类型的派生类。但是比较类对象时我们会发现类对象的比较并不会考虑继承。