20190908 On Java8 第十九章 类型信息
第十九章 类型信息
RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息。
Java 主要有两种方式在运行时识别对象和类信息:
- “传统的” RTTI:假定我们在编译时已经知道了所有的类型;
- “反射”机制:允许我们在运行时发现和使用类的信息。
Class 对象
类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。
类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。
所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。
其实构造器也是类的静态方法,虽然构造器前面并没有 static
关键字。所以,使用 new
操作符创建类的新对象,这个操作也算作对类的静态成员引用。
Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。
类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。
一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。
Class 对象仅在需要的时候才会被加载,static
初始化是在类加载时进行的。
Class 包含很多有用的方法:
传递给 forName()
的字符串必须使用类的全限定名(包含包名)。使用 getName()
来产生完整类名,使用 getSimpleName()
产生不带包名的类名,getCanonicalName()
也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 getName()
相同)。isInterface()
用于判断某个 Class 对象代表的是否为一个接口。Class.getInterface()
方法返回的是存放 Class 对象的数组,里面的 Class 对象表示的是那个类实现的接口。调用 getSuperclass()
方法来得到父类的 Class 对象。Class 对象的 newInstance()
方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。使用 newInstance()
来创建的类,必须带有无参数的构造书。
类字面常量
Java还提供了另一种方法来生成类对象的引用:类字面常量。就像这样:FancyToy.class;
。
类字面常量不仅不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段 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 |
为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
- 链接。在链接阶段将验证类中的字节码,为
static
域分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。 - 初始化。如果该类具有超类,则对其进行初始化,执行
static
初始化器和static
初始化块。
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");
// 不触发初始化:
System.out.println(Initable.STATIC_FINAL);
// 触发初始化:
System.out.println(Initable.STATIC_FINAL2);
// 触发初始化:
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("typeinfo.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 对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
intClass = double.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
// genericIntClass = double.class; // 非法,Type mismatch: cannot convert from Class<Double> to Class<Integer>
}
}
为了在使用 Class 引用时放松限制,可以使用通配符 ?,表示“任何事物”,它是 Java 泛型中的一部分。
使用 Class<?>
比单纯使用 Class 要好,虽然它们是等价的,并且单纯使用 Class 不会产生编译器警告信息。使用 Class<?>
的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。
cast() 方法
Java 中还有用于 Class 引用的转型语法,即 cast()
方法。
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; // ... or just do this.
}
}
Java 类库中另一个没有任何用处的特性就是 Class.asSubclass()
,该方法允许你将一个 Class 对象转型为更加具体的类型。
类型转换检测
关键字 instanceof
。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:
if(x instanceof Dog)
((Dog)x).bark();
instanceof
有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。
一个动态 instanceof 函数
Class.isInstance()
方法提供了一种动态测试对象类型的方法。
递归计数
Class.isAssignableFrom()
:确定此Class对象表示的类或接口是否与指定的Class参数表示的类或接口相同,或者是它们的超类或超接口。如果是,则返回true;否则返回false。
类的等价比较
public class FamilyVsExactType {
static void test(Object x) {
System.out.println("====================================================================");
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)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
反射:运行时类信息
反射提供了检测可用方法并生成方法名称的机制。
类 Class 支持反射的概念,以及 java.lang.reflect
库,其中包含类Field
、Method
和 Constructor
(每一个都实现了 Member
接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 Constructor
创建新对象,get()
和 set()
方法读取和修改与 Field
对象关联的字段,invoke()
方法调用与 Method
对象关联的方法。此外,还可以调用便利方法 getFields()
、getMethods()
、getConstructors()
等,以返回表示字段、方法和构造函数的对象数组。匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。
RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法。通过反射,.class 文件在编译时不可用;它由运行时环境打开并检查。
类方法提取器
Class 方法 getmethods()
和getconstructors()
分别返回Method
数组和Constructor
数组。