|
3 | 3 | <!-- Type Information -->
|
4 | 4 | # 第十九章 类型信息
|
5 | 5 |
|
| 6 | +> RTTI(RunTime Type Information, 运行时类型信息)能够在程序运行时发现和使用类型信息 |
| 7 | +
|
| 8 | +RTTI把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。 |
| 9 | + |
| 10 | +本章将讨论Java是如何在运行时识别对象和类信息的。主要有两种方式: |
| 11 | + |
| 12 | +1. “传统的” RTTI:假定我们在编译时已经知道了所有的类型; |
| 13 | +2. “反射”机制:允许我们在运行时发现和使用类的信息。 |
6 | 14 |
|
7 | 15 | <!-- The Need for RTTI -->
|
8 |
| -## 运行时类型信息 |
9 | 16 |
|
| 17 | +## 为什么需要RTTI |
| 18 | + |
| 19 | +下面看一下我们已经很熟悉的一个例子,它使用了多态的类层次结构。基类`Shape`是泛化的类型,从它派生出了三个具体类: `Circle` 、`Square` 和 `Triangle` (见下图所示)。 |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程的一个基本目的是:让代码只操纵对基类(这里即 `Shape` )的引用。这样,如果你想添加一个新类(比如从`Shape`派生出`Rhomboid`)来扩展程序,就不会影响原来的代码。在这个例子中,`Shape`接口中动态绑定了`draw()`方法,这样做的目的就是让客户端程序员可以使用泛化的`Shape`引用来调用`draw()`。`draw()`方法在所有派生类里都会被覆盖,而且由于它是动态绑定的,所以它可以使用`Shape`引用来调用,这就是多态。 |
| 24 | + |
| 25 | +因此,我们通常会创建一个具体的对象(`Circle`、`Square` 或者`Triangle`),把它向上转型成`Shape`(忽略对象的具体类型),并且在后面的程序中使用`Shape`引用来调用在具体对象中被重载的方法(如`draw()`)。 |
| 26 | + |
| 27 | +代码如下: |
| 28 | + |
| 29 | +```java |
| 30 | +// typeinfo/Shapes.java |
| 31 | +import java.util.stream.*; |
| 32 | + |
| 33 | +abstract class Shape { |
| 34 | + void draw() { System.out.println(this + ".draw()"); } |
| 35 | + @Override |
| 36 | + public abstract String toString(); |
| 37 | +} |
| 38 | + |
| 39 | +class Circle extends Shape { |
| 40 | + @Override |
| 41 | + public String toString() { return "Circle"; } |
| 42 | +} |
| 43 | + |
| 44 | +class Square extends Shape { |
| 45 | + @Override |
| 46 | + public String toString() { return "Square"; } |
| 47 | +} |
| 48 | + |
| 49 | +class Triangle extends Shape { |
| 50 | + @Override |
| 51 | + public String toString() { return "Triangle"; } |
| 52 | +} |
| 53 | + |
| 54 | +public class Shapes { |
| 55 | + public static void main(String[] args) { |
| 56 | + Stream.of( |
| 57 | + new Circle(), new Square(), new Triangle()) |
| 58 | + .forEach(Shape::draw); |
| 59 | + } |
| 60 | +} |
| 61 | +/* Output: |
| 62 | +Circle.draw() |
| 63 | +Square.draw() |
| 64 | +Triangle.draw() |
| 65 | +*/ |
| 66 | +``` |
| 67 | + |
| 68 | +基类中包含`draw()`方法,它通过传递`this`参数传递给`System.out.println()`,间接地使用`toString()`打印类标识符(注意:这里将`toString()`声明为了`abstract`,以此强制继承者覆盖改方法,并防止对`Shape`的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()`方法就会被自动调用,以生成表示该对象的`String`。每个派生类都要覆盖(从`Object`继承来的)`toString()`方法,这样`draw()`在不同情况下就打印出不同的消息(多态)。 |
| 69 | + |
| 70 | +这个例子中,在把`Shape`对象放入`Stream<Shape>`中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对`steam`而言,它们只是`Shape`对象。 |
| 71 | + |
| 72 | +严格来说,`Stream<Shape>`实际上是把放入其中的所有对象都当做`Object`对象来持有,只是取元素时会自动将其类型转为`Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。 |
| 73 | + |
| 74 | +另外在这个例子中,类型转换并不彻底:`Object`被转型为`Shape`,而不是`Circle`、`Square`或者`Triangle`。这是因为目前我们只能确保这个 `Stream<Shape>`保存的都是`Shape`: |
| 75 | + |
| 76 | +- 编译期,`stream`和 Java 泛型系统确保放入`stream`的都是`Shape`对象(`Shape`子类的对象也可视为`Shape`的对象),否则编译器会报错; |
| 77 | +- 运行时,自动类型转换确保了从`stream`中取出的对象都是`Shape`类型。 |
| 78 | + |
| 79 | +接下来就是多态机制的事了,`Shape`对象实际执行什么样的代码,是由引用所指向的具体对象(`Circle`、`Square`或者`Triangle`)决定的。这也符合我们编写代码的一般需求,通常,我们希望大部分代码尽可能少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(本例中即为`Shape`)。这样,代码会更容易写,更易读和维护;设计也更容易实现,更易于理解和修改。所以多态是面向对象的基本目标。 |
| 80 | + |
| 81 | +但是,有时你会碰到一些编程问题,在这些问题中如果你能知道某个泛化引用的具体类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道`Stream<Shape>`里边的形状具体是什么类型,而Java 实际上也满足了我们的这种需求。使用 RTTI,我们可以查询某个`Shape`引用所指向对象的确切类型,然后选择或者剔除特例。 |
10 | 82 |
|
11 | 83 | <!-- The Class Object -->
|
12 |
| -## 类的对象 |
| 84 | + |
| 85 | +## `Class`对象 |
| 86 | + |
| 87 | +要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为**`Class`对象**的特殊对象完成的,它包含了与类有关的信息。实际上,`Class`对象就是用来创建该类所有"常规"对象的。Java使用`Class`对象来实现RTTI,即便是类型转换这样的操作都是用`Class`对象实现的。不仅如此,`Class`类还提供了很多使用RTTI的其它方式。 |
| 88 | + |
| 89 | +类是程序的一部分,每个类都有一个`Class`对象。换言之,每当我们编写并且编译了一个新类,就会产生一个`Class`对象(更恰当的说,是被保存在一个同名的`.class`文件中)。为了生成这个类的对象,Java虚拟机(JVM)先会调用"类加载器"子系统把这个类加载到内存中。 |
| 90 | + |
| 91 | +类加载器子系统可能包含一条类加载器链,但有且只有一个**原生类加载器**,它是JVM实现的一部分。原生类加载器加载的是"可信类"(包括Java API类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。 |
| 92 | + |
| 93 | +所有的类都是第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。 |
| 94 | + |
| 95 | +> 其实构造器也是类的静态方法,虽然构造器前面并没有`static`关键字。所以,使用`new`操作符创建类的新对象,这个操作也算作对类的静态成员引用。 |
| 96 | +
|
| 97 | +因此,Java程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得Java具有一些静态加载语言(如C++)很难或者根本不可能实现的特性。 |
| 98 | + |
| 99 | +类加载器首先会检查这个类的`Class`对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找`.class`文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM会对其进行验证,确保它没有损坏,并且不包含不良的Java代码(这是Java安全防范的一种措施)。 |
| 100 | + |
| 101 | +> 译者注:这里对类加载机制讲得不是很清楚,可以参考《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》第7章 |
| 102 | +
|
| 103 | +一旦某个类的`Class`对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点: |
| 104 | + |
| 105 | +```java |
| 106 | +// typeinfo/SweetShop.java |
| 107 | +// Examination of the way the class loader works |
| 108 | +class Cookie { |
| 109 | + static { System.out.println("Loading Cookie"); } |
| 110 | +} |
| 111 | + |
| 112 | +class Gum { |
| 113 | + static { System.out.println("Loading Gum"); } |
| 114 | +} |
| 115 | + |
| 116 | +class Candy { |
| 117 | + static { System.out.println("Loading Candy"); } |
| 118 | +} |
| 119 | + |
| 120 | +public class SweetShop { |
| 121 | + public static void main(String[] args) { |
| 122 | + System.out.println("inside main"); |
| 123 | + new Candy(); |
| 124 | + System.out.println("After creating Candy"); |
| 125 | + try { |
| 126 | + Class.forName("Gum"); |
| 127 | + } catch(ClassNotFoundException e) { |
| 128 | + System.out.println("Couldn't find Gum"); |
| 129 | + } |
| 130 | + System.out.println("After Class.forName(\"Gum\")"); |
| 131 | + new Cookie(); |
| 132 | + System.out.println("After creating Cookie"); |
| 133 | + } |
| 134 | +} |
| 135 | +/* Output: |
| 136 | +inside main |
| 137 | +Loading Candy |
| 138 | +After creating Candy |
| 139 | +Loading Gum |
| 140 | +After Class.forName("Gum") |
| 141 | +Loading Cookie |
| 142 | +After creating Cookie |
| 143 | +*/ |
| 144 | +``` |
| 145 | + |
| 146 | +上面的代码中,`Candy`、`Gum`和`Cookie`这几个类都有一个`static{...}`静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。而在`main()`里边,创建对象 的代码都放在了`print()`语句之间,以帮助我们判断类加载的时间点。 |
| 147 | + |
| 148 | +从输出中可以看到,`Class`对象仅在需要的时候才会被加载,`static`初始化是在类加载时进行的。 |
| 149 | + |
| 150 | +代码里面还有特别有趣的一行: |
| 151 | + |
| 152 | +```java |
| 153 | +Class.forName("Gum"); |
| 154 | +``` |
| 155 | + |
| 156 | +所有`Class`对象都属于`Class`类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。`forName()`是`Class`类的一个静态方法,我们可以使用`forName()`根据目标类的类名(`String`)得到该类的`Class`对象。上面的代码忽略了`forName()`的返回值,因为那个调用是为了得到它产生的"副作用"。从结果可以看出,`forName()`执行的副作用是如果`Gum`类没有被加载就加载它,而在加载的过程中,`Gum`的`static`初始化块被执行了。 |
| 157 | + |
| 158 | +还需要注意的是,如果`Class.forName()`找不到要加载的类,它就会抛出异常`ClassNotFoundException`。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要在异常处理程序中解决这个问题。 |
| 159 | + |
13 | 160 |
|
14 | 161 |
|
15 | 162 | <!-- Checking Before a Cast -->
|
16 |
| -## 类型转换检测 |
17 | 163 |
|
| 164 | +## 类型转换检测 |
18 | 165 |
|
19 | 166 | <!-- Registered Factories -->
|
| 167 | + |
20 | 168 | ## 注册工厂
|
21 | 169 |
|
22 | 170 |
|
|
0 commit comments