## 泛型擦除
当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 `ArrayList.class`,但不能说成 `ArrayList<Integer>.class`。考虑下面的情况:
```java
// generics/ErasedTypeEquivalence.java
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
/* Output:
true
*/
```
`ArrayList<String>` 和 `ArrayList<Integer>` 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 `ArrayList<String>` 中放入一个 `Integer`,所得到的行为(失败)和向 `ArrayList<Integer>` 中放入一个 `Integer` 所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。
下面的例子是对该谜题的补充:
```java
// generics/LostInformation.java
import java.util.*;
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION, MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> p = new Particle<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/
```
根据 JDK 文档,**Class.getTypeParameters()** “返回一个 **TypeVariable** 对象数组,表示泛型声明中声明的类型参数...” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。
残酷的现实是:
在泛型代码内部,无法获取任何有关泛型参数类型的信息。
因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是 C++ 程序员,那么这个事实会让你很沮丧,在使用 Java 泛型工作时,它是必须处理的最基本的问题。
Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,`List<String>` 和 `List<Integer>` 在运行时实际上是相同的类型。它们都被擦除成原生类型 `List`。
理解擦除并知道如何处理它,是你在学习 Java 泛型时面临的最大障碍之一。这也是本节将要探讨的内容。
### C++ 的方式
下面是使用模版的 C++ 示例。你会看到类型参数的语法十分相似,因为 Java 是受 C++ 启发的:
```c++
// generics/Templates.cpp
#include <iostream>
using namespace std;
template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
/* Output:
HasF::f()
*/
```
**Manipulator** 类存储了一个 **T** 类型的对象。`manipulate()` 方法会调用 **obj** 上的 `f()` 方法。它是如何知道类型参数 **T** 中存在 `f()` 方法的呢?C++ 编译器会在你实例化模版时进行检查,所以在 `Manipulator<HasF>` 实例化的那一刻,它看到 **HasF** 中含有一个方法 `f()`。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。
用 C++ 编写这种代码很简单,因为当模版被实例化时,模版代码就知道模版参数的类型。Java 泛型就不同了。下面是 **HasF** 的 Java 版本:
```java
// generics/HasF.java
public class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
```
如果我们将示例的其余代码用 Java 实现,就不会通过编译:
```java
// generics/Manipulation.java
// {WillNotCompile}
class Manipulator<T> {
private T obj;
Manipulator(T x) {
obj = x;
}
// Error: cannot find symbol: method f():
public void manipulate() {
obj.f();
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
```
因为擦除,Java 编译器无法将 `manipulate()` 方法必须能调用 **obj** 的 `f()` 方法这一需求映射到 HasF 具有 `f()` 方法这个事实上。为了调用 `f()`,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 **extends** 关键字。由于有了边界,下面的代码就能通过编译:
```java
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
```
边界 `<T extends HasF>` 声明 T 必须是 HasF 类型或其子类。如果情况确实如此,就可以安全地在 **obj** 上调用 `f()` 方法。
我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,**T** 擦除到了 **HasF**,就像在类的声明中用 **HasF** 替换了 **T** 一样。
你可能正确地观察到了泛型在 **Manipulator2.java** 中没有贡献任何事。你可以很轻松地自己去执行擦除,生成没有泛型的类:
```java
// generics/Manipulator3.java
class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
```
这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 `<T extends HasF>` 形式就是有缺陷的。例如,如果某个类有一个返回 **T** 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型:
```java
// generics/ReturnGenericType.java
public class ReturnGenericType<T extends HasF> {
private T obj;
ReturnGenericType(T x) {
obj = x;
}
public T get() {
return obj;
}
}
```
你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。
我们将在本章稍后看到有关边界的更多细节。
### 迁移兼容性
为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。它会使你痛苦,因此你需要尽早习惯它并了解为什么它会这样。
如果 Java 1.0 就含有泛型的话,那么这个特性就不会使用擦除来实现——它会使用具体化,保持参数类型为第一类实体,因此你就能在类型参数上执行基于类型的语言操作和反射操作。本章稍后你会看到,擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。
在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, `List<T>` 这样的类型注解会被擦除为 **List**,普通的类型变量在未指定边界的情况下会被擦除为 **Object**。
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。
因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。
例如,假设一个应用使用了两个类库 **X** 和 **Y**,**Y** 使用了类库 **Z**。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型这样的证据必须被”擦除“。
如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的或唯一的迁移途径,还待时间来证明。
### 擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、**instanceof** 操作和 **new** 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
考虑如下的代码段:
```java
class Foo<T> {
T var;
}
```
看上去当你创建一个 **Foo** 实例时:
```java
Foo<Cat> f = new Foo<>();
```
**class** **Foo** 中的代码应该知道现在工作于 **Cat** 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 **Object**“。
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样:
```java
// generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) {
element = arg;
}
public T get() {
return element;
}
}
class Derived1<T> extends GenericBase<T> {}
class Derived2 extends GenericBase {} // No warning
// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type
// required: class or interface without bounds
public class ErasureAndInteritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here!
}
}
```
**Derived2** 继承自 **GenericBase**,但是没有任何类型参数,编译器没有发出任何警告。直到调用 `set()` 方法时才出现警告。
为了关闭警告,Java 提供了一个注解,我们可以在列表中看到它:
```java
@SuppressWarnings("unchecked")
```
这个注解放置在产生警告的方法上,而不是整个类上。当你要关闭警告时,最好尽可能地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。
可以推断,**Derived3** 产生的错误意味着编译器期望得到一个原生基类。
当你希望将类型参数不仅仅当作 Object 处理时,就需要付出额外努力来管理边界,并且与在 C++、Ada 和 Eiffel 这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比 Java 更得心应手,只是说它们的参数化类型机制相比 Java 更灵活、更强大。
### 边界处的动作
因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物。例如:
```java
// generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) {
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker = new ArrayMaker<>(String.class);
String[] stringArray = stringMaker.create(9);
System.out.println(Arrays.toString(stringArray));
}
}
/* Output
[null,null,null,null,null,null,null,null,null]
*/
```
即使 **kind** 被存储为 `Class<T>`,擦除也意味着它实际被存储为没有任何参数的 **Class**。因此,当你在使用它时,例如创建数组,`Array.newInstance()` 实际上并未拥有 **kind** 所蕴含的类型信息。所以它不会产生具体的结果,因而必须转型,这会产生一条令你无法满意的警告。
注意,对于在泛型中创建数组,使用 `Array.newInstance()` 是推荐的方式。
如果我们创建一个集合而不是数组,情况就不同了:
```java
// generics/ListMaker.java
import java.util.*;
public class ListMaker<T> {
List<T> create() {
return new ArrayList<>();
}
public static void main(String[] args) {
ListMaker<String> stringMaker = new ListMaker<>();
List<String> stringList = stringMaker.create();
}
}
```
编译器不会给出任何警告,尽管我们知道(从擦除中)在 `create()` 内部的 `new ArrayList<>()` 中的 `<T>` 被移除了——在运行时,类内部没有任何 `<T>`,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为 `new ArrayList()`,编译器就会发出警告。
本例中这么做真的毫无意义吗?如果在创建 **List** 的同时向其中放入一些对象呢,像这样:
```java
// generics/FilledList.java
import java.util.*;
import java.util.function.*;
import onjava.*;
public class FilledList<T> extends ArrayList<T> {
FilledList(Supplier<T> gen, int size) {
Suppliers.fill(this, gen, size);
}
public FilledList(T t, int size) {
for (int i = 0; i < size; i++) {
this.add(t);
}
}
public static void main(String[] args) {
List<String> list = new FilledList<>("Hello", 4);
System.out.println(list);
// Supplier version:
List<Integer> ilist = new FilledList<>(() -> 47, 4);
System.out.println(ilist);
}
}
/* Output:
[Hello,Hello,Hello,Hello]
[47,47,47,47]
*/
```
即使编译器无法得知 `add()` 中的 **T** 的任何信息,但它仍可以在编译期确保你放入 **FilledList** 中的对象是 **T** 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
因为擦除移除了方法体中的类型信息,所以在运行时的问题就是*边界*:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
考虑如下这段非泛型示例:
```java
// generics/SimpleHolder.java
public class SimpleHolder {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String) holder.get();
}
}
```
如果用 **javap -c SimpleHolder** 反编译这个类,会得到如下内容(经过编辑):
```java
public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; // Field obj:Object;
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; // Field obj:Object;
4: areturn
public static void main(java.lang.String[]);
0: new #3; // class SimpleHolder
3: dup
4: invokespecial #4; // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; // String Item
11: invokevirtual #6; // Method set:(Object;)V
14: aload_1
15: invokevirtual #7; // Method get:()Object;
18: checkcast #8; // class java/lang/String
21: astore_2
22: return
```
`set()` 和 `get()` 方法存储和产生值,转型在调用 `get()` 时接受检查。
现在将泛型融入上例代码中:
```java
// generics/GenericHolder2.java
public class GenericHolder2<T> {
private T obj;
public void set(T obj) {
this.obj = obj;
}
public T get() {
return obj;
}
public static void main(String[] args) {
GenericHolder2<String> holder = new GenericHolder2<>();
holder.set("Item");
String s = holder.get();
}
}
```
从 `get()` 返回后的转型消失了,但是我们还知道传递给 `set()` 的值在编译期会被检查。下面是相关的字节码:
```java
public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; // Field obj:Object;
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; // Field obj:Object;
4: areturn
public static void main(java.lang.String[]);
0: new #3; // class GenericHolder2
3: dup
4: invokespecial #4; // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; // String Item
11: invokevirtual #6; // Method set:(Object;)V
14: aload_1
15: invokevirtual #7; // Method get:()Object;
18: checkcast #8; // class java/lang/String
21: astore_2
22: return
```
所产生的字节码是相同的。对进入 `set()` 的类型进行检查是不需要的,因为这将由编译器执行。而对 `get()` 返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写(阅读)杂乱的代码。
`get()` 和 `set()` 产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。
- 译者的话
- 前言
- 简介
- 第一章 对象的概念
- 抽象
- 接口
- 服务提供
- 封装
- 复用
- 继承
- "是一个"与"像是一个"的关系
- 多态
- 单继承结构
- 集合
- 对象创建与生命周期
- 异常处理
- 本章小结
- 第二章 安装Java和本书用例
- 编辑器
- Shell
- Java安装
- 校验安装
- 安装和运行代码示例
- 第三章 万物皆对象
- 对象操纵
- 对象创建
- 数据存储
- 基本类型的存储
- 高精度数值
- 数组的存储
- 代码注释
- 对象清理
- 作用域
- 对象作用域
- 类的创建
- 类型
- 字段
- 基本类型默认值
- 方法使用
- 返回类型
- 参数列表
- 程序编写
- 命名可见性
- 使用其他组件
- static关键字
- 小试牛刀
- 编译和运行
- 编码风格
- 本章小结
- 第四章 运算符
- 开始使用
- 优先级
- 赋值
- 方法调用中的别名现象
- 算术运算符
- 一元加减运算符
- 递增和递减
- 关系运算符
- 测试对象等价
- 逻辑运算符
- 短路
- 字面值常量
- 下划线
- 指数计数法
- 位运算符
- 移位运算符
- 三元运算符
- 字符串运算符
- 常见陷阱
- 类型转换
- 截断和舍入
- 类型提升
- Java没有sizeof
- 运算符总结
- 本章小结
- 第五章 控制流
- true和false
- if-else
- 迭代语句
- while
- do-while
- for
- 逗号操作符
- for-in 语法
- return
- break 和 continue
- 臭名昭著的 goto
- switch
- switch 字符串
- 本章小结
- 第六章 初始化和清理
- 利用构造器保证初始化
- 方法重载
- 区分重载方法
- 重载与基本类型
- 返回值的重载
- 无参构造器
- this关键字
- 在构造器中调用构造器
- static 的含义
- 垃圾回收器
- finalize()的用途
- 你必须实施清理
- 终结条件
- 垃圾回收器如何工作
- 成员初始化
- 指定初始化
- 构造器初始化
- 初始化的顺序
- 静态数据的初始化
- 显式的静态初始化
- 非静态实例初始化
- 数组初始化
- 动态数组创建
- 可变参数列表
- 枚举类型
- 本章小结
- 第七章 封装
- 包的概念
- 代码组织
- 创建独一无二的包名
- 冲突
- 定制工具库
- 使用 import 改变行为
- 使用包的忠告
- 访问权限修饰符
- 包访问权限
- public: 接口访问权限
- 默认包
- private: 你无法访问
- protected: 继承访问权限
- 包访问权限 Vs Public 构造器
- 接口和实现
- 类访问权限
- 本章小结
- 第八章 复用
- 组合语法
- 继承语法
- 初始化基类
- 带参数的构造函数
- 委托
- 结合组合与继承
- 保证适当的清理
- 名称隐藏
- 组合与继承的选择
- protected
- 向上转型
- 再论组合和继承
- final关键字
- final 数据
- 空白 final
- final 参数
- final 方法
- final 和 private
- final 类
- final 忠告
- 类初始化和加载
- 继承和初始化
- 本章小结
- 第九章 多态
- 向上转型回顾
- 忘掉对象类型
- 转机
- 方法调用绑定
- 产生正确的行为
- 可扩展性
- 陷阱:“重写”私有方法
- 陷阱:属性与静态方法
- 构造器和多态
- 构造器调用顺序
- 继承和清理
- 构造器内部多态方法的行为
- 协变返回类型
- 使用继承设计
- 替代 vs 扩展
- 向下转型与运行时类型信息
- 本章小结
- 第十章 接口
- 抽象类和方法
- 接口创建
- 默认方法
- 多继承
- 接口中的静态方法
- Instrument 作为接口
- 抽象类和接口
- 完全解耦
- 多接口结合
- 使用继承扩展接口
- 结合接口时的命名冲突
- 接口适配
- 接口字段
- 初始化接口中的字段
- 接口嵌套
- 接口和工厂方法模式
- 本章小结
- 第十一章 内部类
- 创建内部类
- 链接外部类
- 使用 .this 和 .new
- 内部类与向上转型
- 内部类方法和作用域
- 匿名内部类
- 嵌套类
- 接口内部的类
- 从多层嵌套类中访问外部类的成员
- 为什么需要内部类
- 闭包与回调
- 内部类与控制框架
- 继承内部类
- 内部类可以被覆盖么?
- 局部内部类
- 内部类标识符
- 本章小结
- 第十二章 集合
- 泛型和类型安全的集合
- 基本概念
- 添加元素组
- 集合的打印
- 迭代器Iterators
- ListIterator
- 链表LinkedList
- 堆栈Stack
- 集合Set
- 映射Map
- 队列Queue
- 优先级队列PriorityQueue
- 集合与迭代器
- for-in和迭代器
- 适配器方法惯用法
- 本章小结
- 简单集合分类
- 第十三章 函数式编程
- 新旧对比
- Lambda表达式
- 递归
- 方法引用
- Runnable接口
- 未绑定的方法引用
- 构造函数引用
- 函数式接口
- 多参数函数式接口
- 缺少基本类型的函数
- 高阶函数
- 闭包
- 作为闭包的内部类
- 函数组合
- 柯里化和部分求值
- 纯函数式编程
- 本章小结
- 第十四章 流式编程
- 流支持
- 流创建
- 随机数流
- int 类型的范围
- generate()
- iterate()
- 流的建造者模式
- Arrays
- 正则表达式
- 中间操作
- 跟踪和调试
- 流元素排序
- 移除元素
- 应用函数到元素
- 在map()中组合流
- Optional类
- 便利函数
- 创建 Optional
- Optional 对象操作
- Optional 流
- 终端操作
- 数组
- 集合
- 组合
- 匹配
- 查找
- 信息
- 数字流信息
- 本章小结
- 第十五章 异常
- 异常概念
- 基本异常
- 异常参数
- 异常捕获
- try 语句块
- 异常处理程序
- 终止与恢复
- 自定义异常
- 异常与记录日志
- 异常声明
- 捕获所有异常
- 多重捕获
- 栈轨迹
- 重新抛出异常
- 精准的重新抛出异常
- 异常链
- Java 标准异常
- 特例:RuntimeException
- 使用 finally 进行清理
- finally 用来做什么?
- 在 return 中使用 finally
- 缺憾:异常丢失
- 异常限制
- 构造器
- Try-With-Resources 用法
- 揭示细节
- 异常匹配
- 其他可选方式
- 历史
- 观点
- 把异常传递给控制台
- 把“被检查的异常”转换为“不检查的异常”
- 异常指南
- 本章小结
- 后记:Exception Bizarro World
- 第十六章 代码校验
- 测试
- 如果没有测试过,它就是不能工作的
- 单元测试
- JUnit
- 测试覆盖率的幻觉
- 前置条件
- 断言(Assertions)
- Java 断言语法
- Guava断言
- 使用断言进行契约式设计
- 检查指令
- 前置条件
- 后置条件
- 不变性
- 放松 DbC 检查或非严格的 DbC
- DbC + 单元测试
- 使用Guava前置条件
- 测试驱动开发
- 测试驱动 vs. 测试优先
- 日志
- 日志会给出正在运行的程序的各种信息
- 日志等级
- 调试
- 使用 JDB 调试
- 图形化调试器
- 基准测试
- 微基准测试
- JMH 的引入
- 剖析和优化
- 优化准则
- 风格检测
- 静态错误分析
- 代码重审
- 结对编程
- 重构
- 重构基石
- 持续集成
- 本章小结
- 第十七章 文件
- 文件和目录路径
- 选取路径部分片段
- 路径分析
- Paths的增减修改
- 目录
- 文件系统
- 路径监听
- 文件查找
- 文件读写
- 本章小结
- 第十八章 字符串
- 字符串的不可变
- +的重载与StringBuilder
- 意外递归
- 字符串操作
- 格式化输出
- printf()
- System.out.format()
- Formatter类
- 格式化修饰符
- Formatter转换
- String.format()
- 一个十六进制转储(dump)工具
- 正则表达式
- 基础
- 创建正则表达式
- 量词
- CharSequence
- Pattern和Matcher
- find()
- 组(Groups)
- start()和end()
- Pattern标记
- split()
- 替换操作
- 正则表达式与 Java I/O
- 扫描输入
- Scanner分隔符
- 用正则表达式扫描
- StringTokenizer类
- 本章小结
- 第十九章 类型信息
- 为什么需要 RTTI
- Class对象
- 类字面常量
- 泛化的Class引用
- cast()方法
- 类型转换检测
- 使用类字面量
- 递归计数
- 一个动态instanceof函数
- 注册工厂
- 类的等价比较
- 反射:运行时类信息
- 类方法提取器
- 动态代理
- Optional类
- 标记接口
- Mock 对象和桩
- 接口和类型
- 本章小结
- 第二十章 泛型
- 简单泛型
- 泛型接口
- 泛型方法
- 复杂模型构建
- 泛型擦除
- 补偿擦除
- 边界
- 通配符
- 问题
- 自限定的类型
- 动态类型安全
- 泛型异常
- 混型
- 潜在类型机制
- 对缺乏潜在类型机制的补偿
- Java8 中的辅助潜在类型
- 总结:类型转换真的如此之糟吗?
- 进阶阅读
- 第二十一章 数组
- 数组特性
- 一等对象
- 返回数组
- 多维数组
- 泛型数组
- Arrays的fill方法
- Arrays的setAll方法
- 增量生成
- 随机生成
- 泛型和基本数组
- 数组元素修改
- 数组并行
- Arrays工具类
- 数组比较
- 数组拷贝
- 流和数组
- 数组排序
- Arrays.sort()的使用
- 并行排序
- binarySearch二分查找
- parallelPrefix并行前缀
- 本章小结
- 第二十二章 枚举
- 基本 enum 特性
- 将静态类型导入用于 enum
- 方法添加
- 覆盖 enum 的方法
- switch 语句中的 enum
- values 方法的神秘之处
- 实现而非继承
- 随机选择
- 使用接口组织枚举
- 使用 EnumSet 替代 Flags
- 使用 EnumMap
- 常量特定方法
- 使用 enum 的职责链
- 使用 enum 的状态机
- 多路分发
- 使用 enum 分发
- 使用常量相关的方法
- 使用 EnumMap 进行分发
- 使用二维数组
- 本章小结
- 第二十三章 注解
- 基本语法
- 定义注解
- 元注解
- 编写注解处理器
- 注解元素
- 默认值限制
- 替代方案
- 注解不支持继承
- 实现处理器
- 使用javac处理注解
- 最简单的处理器
- 更复杂的处理器
- 基于注解的单元测试
- 在 @Unit 中使用泛型
- 实现 @Unit
- 本章小结
- 第二十四章 并发编程
- 术语问题
- 并发的新定义
- 并发的超能力
- 并发为速度而生
- 四句格言
- 1.不要这样做
- 2.没有什么是真的,一切可能都有问题
- 3.它起作用,并不意味着它没有问题
- 4.你必须仍然理解
- 残酷的真相
- 本章其余部分
- 并行流
- 创建和运行任务
- 终止耗时任务
- CompletableFuture类
- 基本用法
- 结合 CompletableFuture
- 模拟
- 异常
- 流异常(Stream Exception)
- 检查性异常
- 死锁
- 构造方法非线程安全
- 复杂性和代价
- 本章小结
- 缺点
- 共享内存陷阱
- This Albatross is Big
- 其他类库
- 考虑为并发设计的语言
- 拓展阅读
- 第二十五章 设计模式
- 概念
- 单例模式
- 模式分类
- 构建应用程序框架
- 面向实现
- 工厂模式
- 动态工厂
- 多态工厂
- 抽象工厂
- 函数对象
- 命令模式
- 策略模式
- 责任链模式
- 改变接口
- 适配器模式(Adapter)
- 外观模式(Façade)
- 包(Package)作为外观模式的变体
- 解释器:运行时的弹性
- 回调
- 多次调度
- 模式重构
- 抽象用法
- 多次派遣
- 访问者模式
- RTTI的优劣
- 本章小结
- 附录:补充
- 附录:编程指南
- 附录:文档注释
- 附录:对象传递和返回
- 附录:流式IO
- 输入流类型
- 输出流类型
- 添加属性和有用的接口
- 通过FilterInputStream 从 InputStream 读取
- 通过 FilterOutputStream 向 OutputStream 写入
- Reader和Writer
- 数据的来源和去处
- 更改流的行为
- 未发生改变的类
- RandomAccessFile类
- IO流典型用途
- 缓冲输入文件
- 从内存输入
- 格式化内存输入
- 基本文件的输出
- 文本文件输出快捷方式
- 存储和恢复数据
- 读写随机访问文件
- 本章小结
- 附录:标准IO
- 附录:新IO
- ByteBuffer
- 数据转换
- 基本类型获取
- 视图缓冲区
- 字节存储次序
- 缓冲区数据操作
- 缓冲区细节
- 内存映射文件
- 性能
- 文件锁定
- 映射文件的部分锁定
- 附录:理解equals和hashCode方法
- 附录:集合主题
- 附录:并发底层原理
- 附录:数据压缩
- 附录:对象序列化
- 附录:静态语言类型检查
- 附录:C++和Java的优良传统
- 附录:成为一名程序员