💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## [`Class`对象](https://lingcoder.gitee.io/onjava8/#/book/19-Type-Information?id=class-%e5%af%b9%e8%b1%a1) 要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为**`Class`对象**的特殊对象完成的,它包含了与类有关的信息。实际上,`Class`对象就是用来创建该类所有"常规"对象的。Java 使用`Class`对象来实现 RTTI,即便是类型转换这样的操作都是用`Class`对象实现的。不仅如此,`Class`类还提供了很多使用 RTTI 的其它方式。 类是程序的一部分,每个类都有一个`Class`对象。换言之,每当我们编写并且编译了一个新类,就会产生一个`Class`对象(更恰当的说,是被保存在一个同名的`.class`文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。 类加载器子系统可能包含一条类加载器链,但有且只有一个**原生类加载器**,它是 JVM 实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。 所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。 > 其实构造器也是类的静态方法,虽然构造器前面并没有`static`关键字。所以,使用`new`操作符创建类的新对象,这个操作也算作对类的静态成员引用。 因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。 类加载器首先会检查这个类的`Class`对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找`.class`文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。 一旦某个类的`Class`对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点: ~~~ // 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 ~~~ 上面的代码中,`Candy`、`Gum`和`Cookie`这几个类都有一个`static{...}`静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。而在主方法里边,创建对象的代码都放在了`print()`语句之间,以帮助我们判断类加载的时间点。 从输出中可以看到,`Class`对象仅在需要的时候才会被加载,`static`初始化是在类加载时进行的。 代码里面还有特别有趣的一行: ~~~ Class.forName("Gum"); ~~~ 所有`Class`对象都属于`Class`类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。`forName()`是`Class`类的一个静态方法,我们可以使用`forName()`根据目标类的类名(`String`)得到该类的`Class`对象。上面的代码忽略了`forName()`的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,`forName()`执行的副作用是如果`Gum`类没有被加载就加载它,而在加载的过程中,`Gum`的`static`初始化块被执行了。 还需要注意的是,如果`Class.forName()`找不到要加载的类,它就会抛出异常`ClassNotFoundException`。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要考虑在异常处理程序中把问题解决掉(具体例子详见[设计模式](https://lingcoder.gitee.io/onjava8/#/./25-Patterns)章节)。 无论何时,只要你想在运行时使用类型信息,就必须先得到那个`Class`对象的引用。`Class.forName()`就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用`getClass()`方法来获取`Class`引用了,这个方法来自根类`Object`,它将返回表示该对象实际类型的`Class`对象的引用。`Class`包含很多有用的方法,下面代码展示了其中的一部分: ~~~ // typeinfo/toys/ToyTest.java // 测试 Class 类 // {java typeinfo.toys.ToyTest} package typeinfo.toys; 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 ~~~ `FancyToy`继承自`Toy`并实现了`HasBatteries`、`Waterproof`和`Shoots`接口。在`main`方法中,我们创建了一个`Class`引用,然后在`try`语句里边用`forName()`方法创建了一个`FancyToy`的类对象并赋值给该引用。需要注意的是,传递给`forName()`的字符串必须使用类的全限定名(包含包名)。 `printInfo()`函数使用`getName()`来产生完整类名,使用`getSimpleName()`产生不带包名的类名,`getCanonicalName()`也是产生完整类名(除内部类和数组外,对大部分类产生的结果与`getName()`相同)。`isInterface()`用于判断某个`Class`对象代表的是否为一个接口。因此,通过`Class`对象,你可以得到关于该类型的所有信息。 在主方法中调用的`Class.getInterfaces()`方法返回的是存放`Class`对象的数组,里面的`Class`对象表示的是那个类实现的接口。 另外,你还可以调用`getSuperclass()`方法来得到父类的`Class`对象,再用父类的`Class`对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。 `Class`对象的`newInstance()`方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。在前面的例子中,`up`只是一个`Class`对象的引用,在编译期并不知道这个引用会指向哪个类的`Class`对象。当你创建新实例时,会得到一个`Object`引用,但是这个引用指向的是`Toy`对象。当然,由于得到的是`Object`引用,目前你只能给它发送`Object`对象能够接受的调用。而如果你想请求具体对象才有的调用,你就得先获取该对象更多的类型信息,并执行某种转型。另外,使用`newInstance()`来创建的类,必须带有无参数的构造器。在本章稍后部分,你将会看到如何通过 Java 的反射 API,用任意的构造器来动态地创建类的对象。