## 泛型擦除
當你開始更深入地鉆研泛型時,會發現有大量的東西初看起來是沒有意義的。例如,盡管可以說 `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的優良傳統
- 附錄:成為一名程序員