RIIT
RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息
Java 是如何在运行时识别对象和类信息的。主要有两种方式:
“传统的” RTTI:假定我们在编译时已经知道了所有的类型;
“反射” 机制:允许我们在运行时发现和使用类的信息
在 Java 中,每个对象都有一个与之关联的类,该类定义了对象的属性和方法 。RTTI 允许程序在运行时确定对象的实际类型,而不是在编译时确定对象的静态类型。这意味着,即使对象的静态类型是某个父类或接口,程序仍然可以根据对象的实际类型来访问它的方法和属性。
在 Java 中,可以使用 instanceof 运算符来检查对象的类型,并根据对象的类型来执行相应的操作。例如:
code 1 2 3 4 5 if (obj instanceof MyClass) { MyClass myObj = (MyClass) obj; myObj.myMethod(); }
上面的代码会检查 obj 对象是否是 MyClass 类型的对象,如果是,就将 obj 强制转换为 MyClass 类型,并调用它的 myMethod 方法。
需要注意的是,如果对象的类型与 instanceof 运算符中指定的类型不兼容,会抛出 ClassCastException 异常。因此,在使用 RTTI 时,应该始终使用强制类型转换之前进行类型检查。
为什么需要 RTTI 机制 、
1 Class<? extends Object> objClass = obj.getClass();
这里涉及到另外一个根本的问题,为什么我们需要在运行时识别对象和类信息。 我们看一个demo
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 class Animal { public void makeSound() { System.out.println("The animal makes a sound"); } } class Dog extends Animal { public void makeSound() { System.out.println("The dog barks"); } } class Cat extends Animal { public void makeSound() { System.out.println("The cat meows"); } } public class Main { public static void main(String[] args) { Animal animal1 = new Animal(); Animal animal2 = new Dog(); Animal animal3 = new Cat(); animal1.makeSound(); animal2.makeSound(); animal3.makeSound(); } }
当使用多态性时,对象的实际类型和调用方法的类型可能不同,需要在运行时识别对象和类信息,在以上的信息中
在上面的示例中,有三个 Animal 对象:animal1、animal2 和 animal3。
animal1 的实际类型是 Animal,animal2 的实际类型是 Dog,animal3 的实际类型是 Cat。 调用 makeSound() 方法时,animal1 会调用 Animal 类的 makeSound() 方法,输出 “The animal makes a sound”;animal2 会调用 Dog 类的 makeSound() 方法,输出 “The dog barks”;animal3 会调用 Cat 类的 makeSound() 方法,输出 “The cat meows”。
因为在编译时,编译器只能确定调用 makeSound() 方法的对象是 Animal 类型的,无法确定其实际类型是什么,因此需要在运行时识别对象和类信息,才能确定调用哪个方法。
通过这个例子,可以看到,运行时识别对象和类信息是多态性的关键,也是 Java 中许多其他特性的基础。
Class 对象引用
要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。 这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。 实际上,Class 对象就是用来创建该类所有 “常规” 对象的。 Java 使用 Class 对象来实现RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。
类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译 了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用 “类加载器” 子系统把 这个类加载到内存中。
类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类 加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数 据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验 证,确保它没有损坏,并且不包含不良的 Java 代码 (这是 Java 安全防范的一种措施)。
一旦某个类的 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 // typeinfo/SweetShop.java // 检查类加载器工作方式 class Cookie { static { System.out.println("Loading Cookie"); } } class Gum { static { System.out.println("Loading Gum"); } } class Candy { static { System.out.println("Loading Candy"); } } public class SweetShop { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After creating Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { System.out.println("Couldn't find Gum"); } System.out.println("After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); } } 输出结果: inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie
Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。 重点的是注意这行代码
forName() 是 Class 类的一个静态方法,我 们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象。上面的 代码忽略了 forName() 的返回值,因为那个调用是为了得到它产生的 “副作用”。 从结果可以看出,forName() 执行的副作用是如果 Gum 类没有被加载就加载它,而在加载的过程中,Gum 的 static 初始化块被执行了。
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 interface HasBatteries {} interface Waterproof {} interface Shoots {} class Toy { // 注释下面的无参数构造器会引起 NoSuchMethodError 错误 Toy() {} Toy(int i) {} } class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots { FancyToy() { super(1); } } public class ToyTest { 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("typeinfo.toys.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 no-arg constructor: obj = up.newInstance(); } catch(InstantiationException e) { System.out.println("Cannot instantiate"); System.exit(1); } catch(IllegalAccessException e) { System.out.println("Cannot access"); System.exit(1); } printInfo(obj.getClass()); } } 输出结果: Class name: typeinfo.toys.FancyToy is interface? [false] Simple name: FancyToy Canonical name : typeinfo.toys.FancyToy Class name: typeinfo.toys.HasBatteries is interface? [true] Simple name: HasBatteries Canonical name : typeinfo.toys.HasBatteries Class name: typeinfo.toys.Waterproof is interface? [true] Simple name: Waterproof Canonical name : typeinfo.toys.Waterproof Class name: typeinfo.toys.Shoots is interface? [true] Simple name: Shoots Canonical name : typeinfo.toys.Shoots Class name: typeinfo.toys.Toy is interface? [false] Simple name: Toy Canonical name : typeinfo.toys.Toy
你还可以调用 getSuperclass() 方法来得到父类的 Class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构
类字面常量 当使用 .class 来创建对 Class 对象的引用时,不会自动地 初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:
加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的 路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。
直到第一次引用一个 static 方法(构造器隐式地是 static)或者非常量的 static 字段,才会进行类初始化。
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 // typeinfo/ClassInitialization.java import java.util.*; 第十九章类型信息 11 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 Exception { Class initable = Initable.class; System.out.println("After creating Initable ref"); // Does not trigger initialization: System.out.println(Initable.STATIC_FINAL); // Does trigger initialization: System.out.println(Initable.STATIC_FINAL2); // Does trigger initialization: System.out.println(Initable2.staticNonFinal); Class initable3 = Class.forName("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
初始化有效地实现了尽可能的 “惰性”,从对 initable 引用的创建中可以看 到,仅使用 .class 语法来获得对类对象的引用不会引发初始化。
但与此相反,使用 Class.forName() 来产生 Class 引用会立即就进行初始化,如 initable3。 如果一个 static final 值是 “编译期常量”(如 Initable.staticFinal),那么这 个值不需要对 Initable 类进行初始化就可以被读取。但是,如果只是将一个字段设置 成为 static 和 final,还不足以确保这种行为。 例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量。 如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之 前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像 在对 Initable2.staticNonFinal 的访问中所看到的那样