本章概要
- Class 对象
- 类字面常量
- 泛化的 Class 引用
- cast() 方法
Class 对象
要理解 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");
}
}
输出结果:
上面的代码中,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
。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要考虑在异常处理程序中把问题解决掉。
无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class
对象的引用。Class.forName()
就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 getClass()
方法来获取 Class
引用了,这个方法来自根类 Object
,它将返回表示该对象实际类型的 Class
对象的引用。Class
包含很多有用的方法,下面代码展示了其中的一部分:
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("com.example.test.ToyTest");
} 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());
}
}
输出结果:
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,用任意的构造器来动态地创建类的对象。
类字面常量
Java 还提供了另一种方法来生成类对象的引用:类字面常量。对上述程序来说,就像这样:FancyToy.class;
。这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不必放在 try
语句块中)。并且它根除了对 forName()
方法的调用,所以效率更高。
类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装类,还有一个标准字段 TYPE
。TYPE
字段是一个引用,指向对应的基本数据类型的 Class
对象,如下所示:
…等价于… | |
---|---|
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
对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个
Class
对象。 - 链接。在链接阶段将验证类中的字节码,为
static
字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。 - 初始化。如果该类具有超类,则先初始化超类,执行
static
初始化器和static
初始化块。
直到第一次引用一个 static
方法(构造器隐式地是 static
)或者非常量的 static
字段,才会进行类初始化。
import java.util.*;
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("com.example.test.Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
输出结果:
初始化有效地实现了尽可能的“惰性”,从对 initable
引用的创建中可以看到,仅使用 .class
语法来获得对类对象的引用不会引发初始化。但与此相反,使用 Class.forName()
来产生 Class
引用会立即就进行初始化,如 initable3
。
如果一个 static final
值是“编译期常量”(如 Initable.staticFinal
),那么这个值不需要对 Initable
类进行初始化就可以被读取。但是,如果只是将一个字段设置成为 static
和 final
,还不足以确保这种行为。例如,对 Initable.staticFinal2
的访问将强制进行类的初始化,因为它不是一个编译期常量。
如果一个 static
字段不是 final
的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像在对 Initable2.staticNonFinal
的访问中所看到的那样。
泛化的 Class
引用
Class
引用总是指向某个 Class
对象,而 Class
对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 static
成员,因此 Class
引用表明了它所指向对象的确切类型,而该对象便是 Class
类的一个对象。
但是,Java 设计者看准机会,将它的类型变得更具体了一些。Java 引入泛型语法之后,我们可以使用泛型对 Class
引用所指向的 Class
对象的类型进行限定。在下面的实例中,两种语法都是正确的:
// typeinfo/GenericClassReferences.java
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // 同一个东西
intClass = double.class;
// genericIntClass = double.class; // 非法
}
}
普通的类引用不会产生警告信息。你可以看到,普通的类引用可以重新赋值指向任何其他的 Class
对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。
那如果我们希望稍微放松一些限制,应该怎么办呢?乍一看,下面的操作好像是可以的:
Class<Number> geenericNumberClass = int.class;
这看起来似乎是起作用的,因为 Integer
继承自 Number
。但事实却是不行,因为 Integer
的 Class
对象并不是 Number
的 Class
对象的子类。
为了在使用 Class
引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ?
,表示“任何事物”。因此,我们可以在上例的普通 Class
引用中添加通配符,并产生相同的结果:
// typeinfo/WildcardClassReferences.java
public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
}
}
使用 Class<?>
比单纯使用 Class
要好,虽然它们是等价的,并且单纯使用 Class
不会产生编译器警告信息。使用 Class<?>
的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。
为了创建一个限定指向某种类型或其子类的 Class
引用,我们需要将通配符与 extends
关键字配合使用,创建一个范围限定。这与仅仅声明 Class<Number>
不同,现在做如下声明:
// typeinfo/BoundedClassReferences.java
public class BoundedClassReferences {
public static void main(String[] args) {
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
// Or anything else derived from Number.
}
}
向 Class
引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点。使用普通的 Class
引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时才能发现它,很不方便。
下面的示例使用了泛型语法,它保存了一个类引用,稍后又用 newInstance()
方法产生类的对象:
import java.util.function.*;
import java.util.stream.*;
class CountedInteger {
private static long counter;
private final long id = counter++;
@Override
public String toString() {
return Long.toString(id);
}
}
public class DynamicSupplier<T> implements Supplier<T> {
private Class<T> type;
public DynamicSupplier(Class<T> type) {
this.type = type;
}
@Override
public T get() {
try {
return type.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Stream.generate(new DynamicSupplier<>(CountedInteger.class))
.skip(10)
.limit(5)
.forEach(System.out::println);
}
}
输出结果:
注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否则运行时会抛出异常。编译期对该程序不会产生任何警告信息。
当你将泛型语法用于 Class
对象时,newInstance()
将返回该对象的确切类型,而不仅仅只是在 ToyTest.java
中看到的基类 Object
。然而,这在某种程度上有些受限:
public class GenericToyTest {
public static void
main(String[] args) throws Exception {
Class<FancyToy> ftClass = FancyToy.class;
// Produces exact type:
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up = ftClass.getSuperclass();
// This won't compile:
// Class<Toy> up2 = ftClass.getSuperclass();
// Only produces Object:
Object obj = up.newInstance();
}
}
如果你手头的是超类,那编译器将只允许你声明超类引用为“某个类,它是 FancyToy
的超类”,就像在表达式 Class<? super FancyToy>
中所看到的那样。而不会接收 Class<Toy>
这样的声明。这看上去显得有些怪,因为 getSuperClass()
方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 Toy.class
),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,up.newInstance
的返回值不是精确类型,而只是 Object
。
cast()
方法
Java 中还有用于 Class
引用的转型语法,即 cast()
方法:
// typeinfo/ClassCasts.java
class Building {}
class House extends Building {}
public class ClassCasts {
public static void main(String[] args) {
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b);
h = (House)b; // ... 或者这样做.
}
}
cast()
方法接受参数对象,并将其类型转换为 Class
引用的类型。但是,如果观察上面的代码,你就会发现,与实现了相同功能的 main
方法中最后一行相比,这种转型好像做了很多额外的工作。
cast()
在无法使用普通类型转换的情况下会显得非常有用,在你编写泛型代码时,如果你保存了 Class
引用,并希望以后通过这个引用来执行转型,你就需要用到 cast()
。但事实却是这种情况非常少见,我发现整个 Java 类库中,只有一处使用了 cast()
(在 com.sun.mirror.util.DeclarationFilter
中)。
Java 类库中另一个没有任何用处的特性就是 Class.asSubclass()
,该方法允许你将一个 Class
对象转型为更加具体的类型。