RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息
RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。
本章将讨论 Java 是如何在运行时识别对象和类信息的。主要有两种方式:
“传统的” RTTI:假定我们在编译时已经知道了所有的类型;
“反射”机制:允许我们在运行时发现和使用类的信息。
import java.util.stream.Stream;
abstract class Shape {
void draw() {
// PS:基类中包含 draw() 方法,它通过传递 this 参数传递给 System.out.println() ,
// 间接地使用 toString() 打印类标识符
System.out.println(this + ".draw()");
}
@Override
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 Triangle extends Shape {
@Override
public String toString() {
return "Triangle";
}
}
public class Shapes {
public static void main(String[] args) {
// PS:在把 Shape 对象放入 Stream 中时就会进行向上转型(隐式)
Stream.of(new Circle(), new Square(), new Triangle())
.forEach(Shape::draw);
}
}
输出:
Circle.draw()
Square.draw()
Triangle.draw()
严格来说, Stream 实际上是把放入其中的所有对象都当做 Object 对象来持有,只是取元素时会自动将其类型转为 Shape 。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型
—PS:多态的应用
Java 使用 Class 对象来实现 RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。
类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。
所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。
其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。
因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。
一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。
Java 还提供了另一种方法来生成类对象的引用:类字面常量(
例如:FancyToy.class)。类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。
当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:
加载,这是由类加载器执行的
链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。
直到第一次引用一个 static 方法(构造器隐式地是 static )或者非常量的 static 字段,才会进行类初始化。
package typeinfo;
import java.util.Random;
class Initable {
static final int STATIC_FINAL = 47;
static final int STATIC_FINAL2 =
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) throws ClassNotFoundException {
// 仅使用 .class 语 法来获得对类对象的引用不会引发初始化
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// 如果一个 static final 值是“编译期常量”(如 Initable.staticFinal ),
// 那么这个值不需要对 Initable 类进行初始化就可以被读取
System.out.println(Initable.STATIC_FINAL);
// PS:对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量,
// Initable.staticFinal2 虽然也是用 static final 修饰
System.out.println(Initable.STATIC_FINAL2);
// 如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之前,
// 要先进行链 接(为这个字段分配存储空间)和初始化(初始化该存储空间)
System.out.println(Initable2.staticNonFinal);
// Class.forName() 来产生 Class 引 用会立即就进行初始化
Class initable3 = Class.forName("typeinfo.Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
输出:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
Java 引入泛型语法之后,我们可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定。在下面的实例中,两种语法都是正确的:
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // 同一个东西
// 普通的类引用可以重新赋值指向任何其他的 Class 对象
intClass = double.class;
// 使用泛型限定的类引用只能指向其声明的类型
// genericIntClass = double.class;
}
}
为了在使用 Class 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ? ,表示“任何事物”。
public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
// 为了创建一个限定指向某种类型或其子类的 Class 引用,
// 我们需要将通配符与 extends 关键字配 合使用,创建一个范围限定。
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
}
}
直到现在,我们已知的 RTTI 类型包括:
传统的类型转换,由 RTTI 确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 ClassCastException 异常。
代表对象类型的 Class 对象. 通过查询 Class 对象可以获取运行时所需的信息。
RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof 。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:
if(x instanceof Dog)
((Dog)x).bark();
在将 x 的类型转换为 Dog 之前, if 语句会先检查 x 是否是 Dog 类型的对象。进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用 instanceof 是非常重要的,否则会得到一个 ClassCastException 异常。
instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。
Class.isInstance() 方法提供了一种动态测试对象类型的方法。
Dog.isInstance(x)
工厂方法可以以多态方式调用,并为你创建适当类型的对象。
当你查询类型信息时,需要注意:instanceof 的形式(即 instanceof 或 isInstance() ,这两者产生的结果相同) 和 与 Class 对象直接比较 这两者间存在重要区别。
class Base {
}
class Derived extends Base {
}
public class FamilyVsExactType {
static void test(Object x) {
System.out.println("Testing x of type " + x.getClass());
System.out.println("x instanceof Base " + (x instanceof Base));
System.out.println("x instanceof Derived " + (x instanceof Derived));
System.out.println("Base.isInstance(x) " + Base.class.isInstance(x));
System.out.println("Derived.isInstance(x) " + Derived.class.isInstance(x));
System.out.println("x.getClass() == Base.class " + (x.getClass() == Base.class));
System.out.println("x.getClass() == Derived.class " + (x.getClass() == Derived.class));
System.out.println("x.getClass().equals(Base.class)) " + (x.getClass().equals(Base.class)));
System.out.println("x.getClass().equals(Derived.class)) " + (x.getClass().equals(Derived.class)));
System.out.println("------------");
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
输出:
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
------------
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
------------
instanceof 和 isInstance() 产生的结果相同, equals() 和 == 产生的结果也相同。但测试本身得出了不同的
结论。与类型的概念一致, instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的 Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。
类 Class 支持反射的概念, java.lang.reflect 库中包含类 Field 、 Method 和 Constructor (每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 Constructor 创建新对象, get() 和 set() 方法读取和修改与 Field 对象关联的字段, invoke() 方法调用与 Method 对象关联的方法。此外,还可以调
用便利方法 getFields() 、 getMethods() 、 getConstructors() 等,以返回表示字段、方法和构造函数的对象数组。
RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以 用“正常”的方式调用一个对象的所有方法。通过反射, .class 文件在编译时不可用;它由运行时环境打开并检查。
RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息
Java 使用 Class 对象来实现 RTTI,保存在 java 文件编译后 .class 文件中
Java 还提供了另一种方法来生成类对象的引用:类字面常量(
例如:FancyToy.class),此时不会自动初始化该 Class 对象
使用类前要做到3个步骤:加载(JVM加载类)、链接(为 static 字段分配存储空间)、初始化(初始化超类及 static 修饰的方法和块)
可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定:
Class<Integer> genericIntClass = int.class;
通配符与 extends 关键字配合使用,创建一个范围限定:
Class<? extends Number> bounded = int.class;
1)instanceof
if(x instanceof Dog)
((Dog)x).bark();
2)isInstance
Dog.isInstance(x)