Skip to content

Commit fd1329a

Browse files
committed
Update CH12.md
1 parent feabae9 commit fd1329a

File tree

1 file changed

+48
-2
lines changed

1 file changed

+48
-2
lines changed

docs/CH12.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class ObjectFoo {
6767

6868
由於 Java 中所有定義的類別,都以 Object 為最上層的父類別,所以用它來實現泛型(Generics)功能是一個不錯的考量,在 J2SE 1.4 或之前版本上,大部份的開發人員會這麼作,您只要撰寫如範例 12.3 的類別,然後可以如下的使用它:
6969

70+
```java
7071
ObjectFoo foo1 = new ObjectFoo();
7172
ObjectFoo foo2 = new ObjectFoo();
7273

@@ -77,12 +78,15 @@ public class ObjectFoo {
7778
foo2.setFoo(new Integer(10));
7879
// 記得轉換操作型態
7980
Integer i = (Integer) foo2.getFoo();
81+
```
8082

8183
看來還不錯,但是設定至 foo1 或 foo2 的 Integer 或 Boolean 實例會失去其型態資訊,從 getFoo() 傳回的是 Object 型態的實例,您必須轉換它的操作型態,問題出在這邊,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換型態時用錯了型態 (像是該用Boolean卻用了Integer),例如:
8284

85+
```java
8386
ObjectFoo foo1 = new ObjectFoo();
8487
foo1.setFoo(new Boolean(true));
8588
String s = (String) foo1.getFoo();
89+
```
8690

8791
由於語法上並沒有錯誤,所以編譯器檢查不出上面的程式有錯誤,真正的錯誤要在執行時期才會發生,這時惱人的 ClassCastException 就會出來搞怪,在使用 Object 設計泛型程式時,程式人員要再細心一些,例如在 J2SE 1.4 或舊版本上,所有存入 List、Map、Set 容器中的實例都會失去其型態資訊,要從這些容器中取回物件並加以操作的話,就得記住取回的物件是什麼型態。
8892

@@ -133,9 +137,11 @@ public class GenericFooDemo {
133137

134138
與單純使用 Object 宣告型態所不同的地方在於,使用泛型所定義的類別在宣告及配置物件時,您可以使用角括號一併指定泛型類別型態持有者 T 真正的型態,而型態或介面轉換就不再需要了,getFoo() 所設定的引數或傳回的型態,就是您在宣告及配置物件時在 `<>` 之間所指定的型態,您所定義出來的泛型類別在使用時多了一層安全性,可以省去惱人的 ClassCastException 發生,編譯器可以幫您作第一層防線,例如下面的程式會被檢查出錯誤:
135139

140+
```java
136141
GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
137142
foo1.setFoo(new Boolean(true));
138143
Integer i = foo1.getFoo(); // 傳回的是Boolean型態
144+
```
139145

140146
foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將這個實例指定給 Integer 型態的變數,顯然在語法上不合,編譯器這時檢查出錯誤:
141147

@@ -146,8 +152,10 @@ foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將
146152

147153
如果使用泛型類別,但宣告及配置物件時不一併指定型態呢?那麼預設會使用 Object 型態,不過您就要自己轉換物件的介面型態了,例如 GenericFoo 可以這麼宣告與使用:
148154

155+
```java
149156
GenericFoo foo3 = new GenericFoo();
150157
foo3.setFoo(new Boolean(false));
158+
```
151159

152160
但編譯時編譯器會提出警訊,告訴您這可能是不安全的操作:
153161

@@ -156,8 +164,10 @@ foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將
156164

157165
回過頭來看看下面的宣告:
158166

167+
```java
159168
GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
160169
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();
170+
```
161171

162172
`GenericFoo<Boolean>` 宣告的 foo1 與 `GenericFoo<Integer>` 宣告的 foo2 是相同的類型嗎?答案是否定的!基本上 foo1 與 foo2 是兩個不同的類型,foo1 是 `GenericFoo<Boolean>` 類型,而 foo2 是 `GenericFoo<Integer>` 類型,所以您不可以將 foo1 所參考的實例指定給 foo2,或是將 foo2 所參考的實例指定給 foo1,要不然編譯器會回報以下錯誤:
163173

@@ -198,8 +208,10 @@ public class GenericFoo2<T1, T2> {
198208

199209
您可以如下使用 GenericFoo2 類別,分別以 Integer 與 Boolean 設定 T1 與 T2 的真正型態:
200210

211+
```java
201212
GenericFoo<Integer, Boolean> foo =
202213
new GenericFoo<Integer, Boolean>();
214+
```
203215
204216
泛型可以用於宣告陣列型態,範例 12.7 是個簡單示範。
205217

@@ -220,24 +232,30 @@ public class GenericFoo3<T> {
220232

221233
您可以像下面的方式來使用範例 12.7 所定義的類別。
222234

223-
`String[] strs = {"caterpillar", "momor", "bush"};
235+
```java
236+
String[] strs = {"caterpillar", "momor", "bush"};
224237
GenericFoo3<String> foo = new GenericFoo3<String>();
225238
foo.setFooArray(strs);
226239
strs = foo.getFooArray();
240+
```
227241

228242
注意您可以使用泛型機制來宣告一個陣列,例如下面這樣是可行的:
229243

244+
```java
230245
public class GenericFoo<T> {
231246
private T[] fooArray;
232247
// ...
233248
}
249+
```
234250

235251
但是您不可以使用泛型來建立陣列的實例,例如以下是不可行的:
236252

253+
```java
237254
public class GenericFoo<T> {
238255
private T[] fooArray = new T[10]; // 不可以使用泛型建立陣列實例
239256
// ...
240257
}
258+
```
241259

242260
如果您已經定義了一個泛型類別,想要用這個類別在另一個泛型類別中宣告成員的話要如何作?舉個實例,假設您已經定義了範例 12.4 的類別,現在想要設計一個新的類別,當中包括了範例12.4的類別實例作為其成員,您可以如範例 12.8 的方式設計。
243261

@@ -258,10 +276,12 @@ public class WrapperFoo<T> {
258276

259277
這麼一來,您就可以保留型態持有者T的功能,一個使用的例子如下:
260278

279+
```java
261280
GenericFoo<Integer> foo = new GenericFoo<Integer>();
262281
foo.setFoo(new Integer(10));
263282
WrapperFoo<Integer> wrapper = new WrapperFoo<Integer>();
264283
wrapper.setFoo(foo);
284+
```
265285

266286
## 12.2 泛型進階語法
267287

@@ -292,32 +312,42 @@ public class ListGenericFoo<T extends List> {
292312

293313
ListGenericFoo 在宣告類型持有者時,一併指定這個持有者實例化的對象,必須是實作 java.util.List 介面(interface)的類別,在限定持有者時,無論是要限定的對象是介面或類別,都是使用 "extends" 關鍵字,範例中您使用 "extends" 限定型態持有者實例化的對象,必須是實作 List 介面的類別,像 java.util.LinkedList 與 java.util.ArrayList 就實作了 List 介面(第 13 章就會介紹),例如下面的程式片段是合法的使用方式:
294314

315+
```java
295316
ListGenericFoo<LinkedList> foo1 =
296317
new ListGenericFoo<LinkedList>();
297318
ListGenericFoo<ArrayList> foo2 =
298319
new ListGenericFoo<ArrayList>();
299320

321+
```
300322
但如果不是實作 List 的類別,編譯時就會發生錯誤,例如下面的程式片段通不過編譯:
301323

324+
```java
302325
ListGenericFoo<HashMap> foo3 =
303326
new ListGenericFoo<HashMap>();
327+
```
304328

305329
因為 java.util.HashMap 並沒有實作 List 介面(事實上 HashMap 實作了 Map 介面),編譯器會在編譯時期就檢查出這個錯誤:
306330

331+
```java
307332
type parameter java.util.HashMap is not within its bound
308333
ListGenericFoo<HashMap> foo3 = new ListGenericFoo<HashMap>();
334+
```
309335

310336
HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者的對象,事實上,當您沒有使用 "extends" 關鍵字限定型態持有者時,預設是 Object 下的所有子類別都可以實例化型態持有者,也就是說在您定義泛型類別時如果只寫以下的話:
311337

338+
```java
312339
public class GenericFoo<T> {
313340
//....
314341
}
342+
```
315343

316344
其實就相當於以下的定義方式:
317345

346+
```java
318347
public class GenericFoo<T extends Object> {
319348
//....
320349
}
350+
```
321351

322352
由於 Java 中所有的實例都繼承自 Object 類別,所以定義時若只寫 `<T>` 就表示,所有類型的物件都可以實例化您所定義的泛型類別。
323353

@@ -344,27 +374,35 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者
344374

345375
仍然以範例 12.4 所定義的 GenericFoo 來進行說明,假設您使用 GenericFoo 類別來如下宣告名稱:
346376

377+
```java
347378
GenericFoo<Integer> foo1 = null;
348379
GenericFoo<Boolean> foo2 = null;
380+
```
349381

350382
那麼名稱 foo1 就只能參考 `GenericFoo<Integer>` 類型的實例,而名稱 foo2 只能參考 `GenericFoo<Boolean>` 類型的實例,也就是說下面的方式是可行的:
351383

384+
```java
352385
foo1 = new GenericFoo<Integer>();
353386
foo2 = new GenericFoo<Boolean>();
387+
```
354388

355389
現在您有這麼一個需求,您希望有一個參考名稱 foo 可以如下接受所指定的實例:
356390

391+
```java
357392
foo = new GenericFoo<ArrayList>();
358393
foo = new GenericFoo<LinkedList>();
394+
```
359395

360396
簡單的說,您想要有一個 foo 名稱可以參考的對象,其型態持有者實例化的對象是實作 List 介面的類別或其子類別,要宣告這麼一個參考名稱,您可以使用 '?'「通配字元」(Wildcard),'?' 代表未知型態,並使用 "extends" 關鍵字來作限定,例如:
361397

398+
```java
362399
GenericFoo<? extends List> foo = null;
363400
foo = new GenericFoo<ArrayList>();
364401
.....
365402
foo = new GenericFoo<LinkedList>();
366403
....
367-
404+
```
405+
368406
`<? extends List>` 表示型態未知,只知會是實作 List 介面的類別,所以如果型態持有者實例化的對象不是實作 List 介面的類別,則編譯器會回報錯誤,例如以下這行無法通過編譯:
369407

370408
GenericFoo<? extends List> foo = new GenericFoo<HashMap>();
@@ -378,19 +416,24 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者
378416

379417
使用 '?' 來作限定有時是很有用的,例如若您想要自訂一個 showFoo() 方法,方法的內容實作是針對 String 或其子類的實例而制定的,例如:
380418

419+
```java
381420
public void showFoo(GenericFoo foo) {
382421
// 針對String或其子類而制定的內容
383422
}
423+
```
384424

385425
如果只作以上的宣告,那麼像 `GenericFoo<Integer>``GenericFoo<Boolean>` 等型態都可以傳入至方法中,如果您不希望任何的型態都可以傳 入showFoo() 方法中,您可以使用以下的方式來限定:
386426

427+
```java
387428
public void showFoo(GenericFoo<? extends String> foo) {
388429
// 針對String或其子類而制定的內容,例如下面這行
389430
System.out.println(foo.getFoo());
390431
}
432+
```
391433

392434
這麼一來,如果有粗心的程式設計人員傳入了您不想要的型態,例如 `GenericFoo<Boolean>` 型態的實例,則編譯器都會告訴它這是不可行的,在宣告名稱時如果指定了 `<?>` 而不使用 "extends",則預設是允許 Object 及其下的子類,也就是所有的 Java 物件了,那為什麼不直接使用 `GenericFoo` 宣告就好了,何必要用 `GenericFoo<?>` 來宣告?使用通配字元有點要注意的是,透過使用通配字元宣告的名稱所參考的物件,您沒辦法再對它加入新的資訊,您只能取得它當中的資訊或是移除當中的資訊,例如:
393435

436+
```java
394437
GenericFoo<String> foo = new GenericFoo<String>();
395438
foo.setFoo("caterpillar");
396439

@@ -404,12 +447,15 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者
404447
// 不可透過immutableFoo來設定新的資訊給foo所參考的實例
405448
// 所以下面這行無法通過編譯
406449
// immutableFoo.setFoo("良葛格");
450+
```
407451

408452
所以使用 `<?>` 或是 `<? extends SomeClass>` 的宣告方式,意味著您只能透過該名稱來取得所參考實例的資訊,或者是移除某些資訊,但不能增加它的資訊,理由很簡單,因為您不知道 `<?>` 或是 `<? extends SomeClass>` 宣告的參考名稱,實際上參考的物件,當中確實儲存的是什麼類型的資訊,基於泛型的設計理念,當然也就沒有理由能加入新的資訊了,因為若能加入,被加入的物件同樣也會有失去型態資訊的問題。
409453

410454
除了可以向下限制,您也可以向上限制,只要使用 "super" 關鍵字,例如:
411455

456+
```java
412457
GenericFoo<? super StringBuilder> foo = null;
458+
```
413459

414460
如此, foo 就只接受 StringBuilder 及其上層的父類型態,也就是只能接受 `GenericFoo<StringBuilder>``GenericFoo<Object>` 的實例。
415461

0 commit comments

Comments
 (0)