From cf5c2e78d6aef4af6f0c040eb924e53a714e5261 Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 09:44:17 +0800 Subject: [PATCH 1/6] Update CH10.md --- docs/CH10.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/CH10.md b/docs/CH10.md index bff504a..2145377 100644 --- a/docs/CH10.md +++ b/docs/CH10.md @@ -9,13 +9,14 @@ ## 10.1 例外處理入門 回憶一下 6.2.1 中介紹過的「命令列引數」(Command line argument),在還沒有學會使用Java的例外處理之前,為了確定使用者是否有提供引數,您可能要先檢查引數的陣列長度是否為 0,以免程式存取超過陣列長度而發生錯誤: - +```java if(args.length == 0) { // 使用者沒有指定引數,顯示引數功能畫面 } else { // 執行所指定引數的對應功能 } +``` 利用條件判斷式來避免錯誤的發生,這樣的檢查方式在一些程式語言中經常出現,然而顯然的,處理錯誤的邏輯與處理業務的邏輯混在一起,如果更多的錯誤狀況必須檢查的話,程式會更難以閱讀,且由於使用了一些判斷式,即使發生機率低的錯誤,也都必須一視同仁的進行判斷檢查,這會使得程式的執行效能受到一定程度的影響。 @@ -23,6 +24,7 @@ Java 的例外處理機制可以協助您避開或是處理程式可能發生的 在 Java 中如果想嘗試捕捉例外,可以使用 "try"、"catch"、"finally" 三個關鍵字組合的語法來達到,其語法基本結構如下: +```java try { // 陳述句 } @@ -32,6 +34,7 @@ Java 的例外處理機制可以協助您避開或是處理程式可能發生的 finally { // 一定會處理的區塊 } +``` 一個 "try" 語法所包括的區塊,必須有對應的 "catch" 區塊或是 "finally" 區塊,"try" 區塊可以搭配多個 "catch" 區塊,如果有設定 "catch" 區塊,則 "finally" 區塊可有可無,如果沒有定義 "catch" 區塊,則一定要有 "finally" 區塊。 @@ -61,6 +64,7 @@ public class CheckArgsDemo { 範例 10.1 中並沒有使用條件判斷式來檢查陣列長度,也就是沒有使用 if 陳述句,例外處理只有在錯誤真正發生,也就是丟出例外時才處理,所以與使用 if 判斷式每次都要進行檢查動作相比,效率上會好一些,要注意的是,例外處理最好只用於錯誤處理,而不應是用於程式業務邏輯的一部份,因為例外的產生要消耗資源,例如以下應用例外處理的方式就不適當: +```java while(true) { try { System.out.println(args[i]); @@ -70,12 +74,15 @@ public class CheckArgsDemo { break; } } +``` 循序取出陣列值時,最後一定會到達陣列的邊界,檢查邊界是必要的動作,是程式業務邏輯的一部份,而不是錯誤處理邏輯的一部份,您該使用的是 for 迴圈而不是依賴例外處理,例如下面的方式才是正確的: +```java for(int i = 0; i < args.length; i++) { System.out.println(args[i]); } +``` ## 10.2 受檢例外(Checked Exception)、執行時期例外(Runtime Exception) From feabae98f298d099f8ccffdfbf7fbdcdc797112f Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 10:26:06 +0800 Subject: [PATCH 2/6] Update CH11.md --- docs/CH11.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/CH11.md b/docs/CH11.md index c4cd6ab..0ad72b1 100644 --- a/docs/CH11.md +++ b/docs/CH11.md @@ -25,6 +25,7 @@ public interface ActionConstants { 共用的常數通常是可以直接取用並且不可被修改的,所以您在宣告時加上 "static" 與 "final",如此您可以在程式中直接使用像是 ActionConstants.TURN_LEFT 的名稱來取用常數值,例如: +```java public void someMethod() { .... doAction(ActionConstants.TURN_RIGHT); @@ -43,6 +44,7 @@ public interface ActionConstants { break; } } +``` 如果使用類別來宣告的話,方法也是類似,例如: @@ -68,6 +70,7 @@ public class CommandTool { 您已經知道可以在類別或介面中宣告常數來統一管理常數,這只是讓您存取與管理常數方便而已,來看看這個例子: +```java public void someMethod() { .... doAction(ActionConstants.TURN_RIGHT); @@ -81,6 +84,7 @@ public class CommandTool { .. } } +``` 這種作法本身沒錯,只不過 doAction() 方法接受的是int型態的常數,您沒有能力阻止程式設計人員對它輸入 ActionConstants 規定外的其它常數,也沒有檢查 "switch" 中列舉的值是不是正確的值,因為參數 action 就只是 int 型態而已,當然您可以自行設計一些檢查動作,這需要一些額外的工作,如果您使用 J2SE 5.0 中新增的「列舉型態」(Enumerated Types),就可以無需花額外的功夫就輕易的解決這些問題。 @@ -88,11 +92,13 @@ public class CommandTool { #### **範例 11.3 Action.java** +```java public enum Action { TURN_LEFT, TURN_RIGHT, SHOOT } +``` 不用懷疑,在 Action.java 中撰寫範例 11.3 的內容然後編譯它,雖然語法上不像是在定義類別,但列舉型態骨子裏就是一個類別,所以您編譯完成後,會產生一個 Action.class 檔案。 @@ -128,6 +134,7 @@ public class EnumDemo { 除了讓您少打一些字之外,這個範例好像沒有什麼特別的,但注意到 doAction() 參數列的型態是 Action,如果您對 doAction() 方法輸入其它型態的引數,編譯器會回報錯誤,因為 doAction() 所接受的引數必須是 Action 列舉型態。 使用列舉型態還可以作到更進一步的檢驗,如果您在 "switch" 中加入了不屬於 Action 中列舉的值,編譯器也會回報錯誤,例如: +```java ... public static void doAction(Action action) { switch(action) { @@ -145,6 +152,7 @@ public class EnumDemo { break; } } ... +``` 在編譯時編譯器會替您作檢查,若檢查出不屬於 Action 中的列舉值,會顯示以下的錯誤: From fd1329a967cebd40669e9094526a3c87165b28a0 Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 11:19:06 +0800 Subject: [PATCH 3/6] Update CH12.md --- docs/CH12.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/CH12.md b/docs/CH12.md index 60afc64..4de0a93 100644 --- a/docs/CH12.md +++ b/docs/CH12.md @@ -67,6 +67,7 @@ public class ObjectFoo { 由於 Java 中所有定義的類別,都以 Object 為最上層的父類別,所以用它來實現泛型(Generics)功能是一個不錯的考量,在 J2SE 1.4 或之前版本上,大部份的開發人員會這麼作,您只要撰寫如範例 12.3 的類別,然後可以如下的使用它: +```java ObjectFoo foo1 = new ObjectFoo(); ObjectFoo foo2 = new ObjectFoo(); @@ -77,12 +78,15 @@ public class ObjectFoo { foo2.setFoo(new Integer(10)); // 記得轉換操作型態 Integer i = (Integer) foo2.getFoo(); +``` 看來還不錯,但是設定至 foo1 或 foo2 的 Integer 或 Boolean 實例會失去其型態資訊,從 getFoo() 傳回的是 Object 型態的實例,您必須轉換它的操作型態,問題出在這邊,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換型態時用錯了型態 (像是該用Boolean卻用了Integer),例如: +```java ObjectFoo foo1 = new ObjectFoo(); foo1.setFoo(new Boolean(true)); String s = (String) foo1.getFoo(); +``` 由於語法上並沒有錯誤,所以編譯器檢查不出上面的程式有錯誤,真正的錯誤要在執行時期才會發生,這時惱人的 ClassCastException 就會出來搞怪,在使用 Object 設計泛型程式時,程式人員要再細心一些,例如在 J2SE 1.4 或舊版本上,所有存入 List、Map、Set 容器中的實例都會失去其型態資訊,要從這些容器中取回物件並加以操作的話,就得記住取回的物件是什麼型態。 @@ -133,9 +137,11 @@ public class GenericFooDemo { 與單純使用 Object 宣告型態所不同的地方在於,使用泛型所定義的類別在宣告及配置物件時,您可以使用角括號一併指定泛型類別型態持有者 T 真正的型態,而型態或介面轉換就不再需要了,getFoo() 所設定的引數或傳回的型態,就是您在宣告及配置物件時在 `<>` 之間所指定的型態,您所定義出來的泛型類別在使用時多了一層安全性,可以省去惱人的 ClassCastException 發生,編譯器可以幫您作第一層防線,例如下面的程式會被檢查出錯誤: +```java GenericFoo foo1 = new GenericFoo(); foo1.setFoo(new Boolean(true)); Integer i = foo1.getFoo(); // 傳回的是Boolean型態 +``` foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將這個實例指定給 Integer 型態的變數,顯然在語法上不合,編譯器這時檢查出錯誤: @@ -146,8 +152,10 @@ foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將 如果使用泛型類別,但宣告及配置物件時不一併指定型態呢?那麼預設會使用 Object 型態,不過您就要自己轉換物件的介面型態了,例如 GenericFoo 可以這麼宣告與使用: +```java GenericFoo foo3 = new GenericFoo(); foo3.setFoo(new Boolean(false)); +``` 但編譯時編譯器會提出警訊,告訴您這可能是不安全的操作: @@ -156,8 +164,10 @@ foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將 回過頭來看看下面的宣告: +```java GenericFoo foo1 = new GenericFoo(); GenericFoo foo2 = new GenericFoo(); +``` `GenericFoo` 宣告的 foo1 與 `GenericFoo` 宣告的 foo2 是相同的類型嗎?答案是否定的!基本上 foo1 與 foo2 是兩個不同的類型,foo1 是 `GenericFoo` 類型,而 foo2 是 `GenericFoo` 類型,所以您不可以將 foo1 所參考的實例指定給 foo2,或是將 foo2 所參考的實例指定給 foo1,要不然編譯器會回報以下錯誤: @@ -198,8 +208,10 @@ public class GenericFoo2 { 您可以如下使用 GenericFoo2 類別,分別以 Integer 與 Boolean 設定 T1 與 T2 的真正型態: +```java GenericFoo foo = new GenericFoo(); +``` 泛型可以用於宣告陣列型態,範例 12.7 是個簡單示範。 @@ -220,24 +232,30 @@ public class GenericFoo3 { 您可以像下面的方式來使用範例 12.7 所定義的類別。 - `String[] strs = {"caterpillar", "momor", "bush"}; +```java + String[] strs = {"caterpillar", "momor", "bush"}; GenericFoo3 foo = new GenericFoo3(); foo.setFooArray(strs); strs = foo.getFooArray(); +``` 注意您可以使用泛型機制來宣告一個陣列,例如下面這樣是可行的: +```java public class GenericFoo { private T[] fooArray; // ... } +``` 但是您不可以使用泛型來建立陣列的實例,例如以下是不可行的: +```java public class GenericFoo { private T[] fooArray = new T[10]; // 不可以使用泛型建立陣列實例 // ... } +``` 如果您已經定義了一個泛型類別,想要用這個類別在另一個泛型類別中宣告成員的話要如何作?舉個實例,假設您已經定義了範例 12.4 的類別,現在想要設計一個新的類別,當中包括了範例12.4的類別實例作為其成員,您可以如範例 12.8 的方式設計。 @@ -258,10 +276,12 @@ public class WrapperFoo { 這麼一來,您就可以保留型態持有者T的功能,一個使用的例子如下: +```java GenericFoo foo = new GenericFoo(); foo.setFoo(new Integer(10)); WrapperFoo wrapper = new WrapperFoo(); wrapper.setFoo(foo); +``` ## 12.2 泛型進階語法 @@ -292,32 +312,42 @@ public class ListGenericFoo { ListGenericFoo 在宣告類型持有者時,一併指定這個持有者實例化的對象,必須是實作 java.util.List 介面(interface)的類別,在限定持有者時,無論是要限定的對象是介面或類別,都是使用 "extends" 關鍵字,範例中您使用 "extends" 限定型態持有者實例化的對象,必須是實作 List 介面的類別,像 java.util.LinkedList 與 java.util.ArrayList 就實作了 List 介面(第 13 章就會介紹),例如下面的程式片段是合法的使用方式: +```java ListGenericFoo foo1 = new ListGenericFoo(); ListGenericFoo foo2 = new ListGenericFoo(); +``` 但如果不是實作 List 的類別,編譯時就會發生錯誤,例如下面的程式片段通不過編譯: +```java ListGenericFoo foo3 = new ListGenericFoo(); +``` 因為 java.util.HashMap 並沒有實作 List 介面(事實上 HashMap 實作了 Map 介面),編譯器會在編譯時期就檢查出這個錯誤: +```java type parameter java.util.HashMap is not within its bound ListGenericFoo foo3 = new ListGenericFoo(); +``` HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者的對象,事實上,當您沒有使用 "extends" 關鍵字限定型態持有者時,預設是 Object 下的所有子類別都可以實例化型態持有者,也就是說在您定義泛型類別時如果只寫以下的話: +```java public class GenericFoo { //.... } +``` 其實就相當於以下的定義方式: +```java public class GenericFoo { //.... } +``` 由於 Java 中所有的實例都繼承自 Object 類別,所以定義時若只寫 `` 就表示,所有類型的物件都可以實例化您所定義的泛型類別。 @@ -344,27 +374,35 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者 仍然以範例 12.4 所定義的 GenericFoo 來進行說明,假設您使用 GenericFoo 類別來如下宣告名稱: +```java GenericFoo foo1 = null; GenericFoo foo2 = null; +``` 那麼名稱 foo1 就只能參考 `GenericFoo` 類型的實例,而名稱 foo2 只能參考 `GenericFoo` 類型的實例,也就是說下面的方式是可行的: +```java foo1 = new GenericFoo(); foo2 = new GenericFoo(); +``` 現在您有這麼一個需求,您希望有一個參考名稱 foo 可以如下接受所指定的實例: +```java foo = new GenericFoo(); foo = new GenericFoo(); +``` 簡單的說,您想要有一個 foo 名稱可以參考的對象,其型態持有者實例化的對象是實作 List 介面的類別或其子類別,要宣告這麼一個參考名稱,您可以使用 '?'「通配字元」(Wildcard),'?' 代表未知型態,並使用 "extends" 關鍵字來作限定,例如: +```java GenericFoo foo = null; foo = new GenericFoo(); ..... foo = new GenericFoo(); .... - +``` + `` 表示型態未知,只知會是實作 List 介面的類別,所以如果型態持有者實例化的對象不是實作 List 介面的類別,則編譯器會回報錯誤,例如以下這行無法通過編譯: GenericFoo foo = new GenericFoo(); @@ -378,19 +416,24 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者 使用 '?' 來作限定有時是很有用的,例如若您想要自訂一個 showFoo() 方法,方法的內容實作是針對 String 或其子類的實例而制定的,例如: +```java public void showFoo(GenericFoo foo) { // 針對String或其子類而制定的內容 } +``` 如果只作以上的宣告,那麼像 `GenericFoo`、`GenericFoo` 等型態都可以傳入至方法中,如果您不希望任何的型態都可以傳 入showFoo() 方法中,您可以使用以下的方式來限定: +```java public void showFoo(GenericFoo foo) { // 針對String或其子類而制定的內容,例如下面這行 System.out.println(foo.getFoo()); } +``` 這麼一來,如果有粗心的程式設計人員傳入了您不想要的型態,例如 `GenericFoo` 型態的實例,則編譯器都會告訴它這是不可行的,在宣告名稱時如果指定了 `` 而不使用 "extends",則預設是允許 Object 及其下的子類,也就是所有的 Java 物件了,那為什麼不直接使用 `GenericFoo` 宣告就好了,何必要用 `GenericFoo` 來宣告?使用通配字元有點要注意的是,透過使用通配字元宣告的名稱所參考的物件,您沒辦法再對它加入新的資訊,您只能取得它當中的資訊或是移除當中的資訊,例如: +```java GenericFoo foo = new GenericFoo(); foo.setFoo("caterpillar"); @@ -404,12 +447,15 @@ HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者 // 不可透過immutableFoo來設定新的資訊給foo所參考的實例 // 所以下面這行無法通過編譯 // immutableFoo.setFoo("良葛格"); +``` 所以使用 `` 或是 `` 的宣告方式,意味著您只能透過該名稱來取得所參考實例的資訊,或者是移除某些資訊,但不能增加它的資訊,理由很簡單,因為您不知道 `` 或是 `` 宣告的參考名稱,實際上參考的物件,當中確實儲存的是什麼類型的資訊,基於泛型的設計理念,當然也就沒有理由能加入新的資訊了,因為若能加入,被加入的物件同樣也會有失去型態資訊的問題。 除了可以向下限制,您也可以向上限制,只要使用 "super" 關鍵字,例如: +```java GenericFoo foo = null; +``` 如此, foo 就只接受 StringBuilder 及其上層的父類型態,也就是只能接受 `GenericFoo` 與 `GenericFoo` 的實例。 From ca3b4f77bf2c60a68b13778d52df93ac881b2b9b Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 11:44:54 +0800 Subject: [PATCH 4/6] Update CH13.md --- docs/CH13.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/CH13.md b/docs/CH13.md index 6c6c94c..3ec373e 100644 --- a/docs/CH13.md +++ b/docs/CH13.md @@ -14,22 +14,26 @@ Collection 結構可持有各自獨立的物件,在 Java SE 中, Collection java.util.ArrayList 類別實作了 java.util.List 介面,所以要先認識一下 List 介面,List 介面是 java.util.Collection 介面的子介面,而 Collection 介面則是 java.lang.Iterable 的子介面,Iterable 介面要求實作一個 iterator() 方法。 +```java package java.lang; import java.util.Iterator; public interface Iterable { Iterator iterator(); } +``` 從 J2SE 5.0 開始增加了泛型設計的新功能,所以像 Iterable、Collection 相關介面與其實作類別,都使用泛型的功能重新改寫了,因而您可以在原始碼或是 API 文件中看到增加了不少與泛型相關的功能或說明。 Iterable 介面要求實作它的類別傳回一個實作 java.util.Iterator 介面的物件,事實上您在 Java SE 的 API 中找不到任何實作 Iterator 的類別,因為 Iterator 會根據實際的容器資料結構來迭代元素,而容器的資料結構實作方式對外界是隱藏的,使用者不用知道這個結構,只需要知道 Iterator 的操作方法,就可以取出元素,Iterator 介面的定義如下: +```java package java.util; public interface Iterator { boolean hasNext(); E next(); void remove(); } +``` > **良葛格的話匣子** Iterator 是「Iterator 模式」的一個實例,有關 Iterator 模式,請參考我網站上的文件: > @@ -37,6 +41,7 @@ Iterable 介面要求實作它的類別傳回一個實作 java.util.Iterator 介 Collection 介面繼承了 Iterator 介面,定義了加入元素、移除元素、元素長度等方法, +```java package java.util; public interface Collection extends Iterable { int size(); @@ -54,9 +59,11 @@ Collection 介面繼承了 Iterator 介面,定義了加入元素、移除元 boolean equals(Object o); int hashCode(); } +``` Collection 在移除元素及取得元素上的定義是比較通用,List 介面則又增加了根據索引取得物件的方法,這說明了 List 資料結構的特性,每個加入 List 中的元素是循序加入的,並可指定索引來存取元素(以下原始碼只是節錄部份)。 +```java package java.util; public interface List extends Collection { .... @@ -70,6 +77,7 @@ Collection 在移除元素及取得元素上的定義是比較通用,List 介 List subList(int fromIndex, int toIndex); .... } +``` List 資料結構的特性是,每個加入 List 中的元素是循序加入的,並可指定索引來存取元素,List 可以使用陣列(Array)或是鏈結串列(Linked List)來實作這個特性,前者在 Java SE 中的實作就是 java.util.ArrayList,後者就是 java.util.LinkedList,對於循序加入與存取,使用 ArrayList 的效率比較好,對於經常變動元素排列順序的需求,使用 LinkedList 會比較好。 From a220f5808e79a47f1380edf212fdfd79b9833257 Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 14:56:35 +0800 Subject: [PATCH 5/6] Update CH08.md --- docs/CH08.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/CH08.md b/docs/CH08.md index d811cb2..0530bf3 100644 --- a/docs/CH08.md +++ b/docs/CH08.md @@ -195,6 +195,7 @@ public class Cubic extends Rectangle { 舉個例子來說,看看下面這個類別: +```java public class SimpleArray { protected int[] array; @@ -206,9 +207,11 @@ public class Cubic extends Rectangle { } .... } +``` 這個類別設計一個簡單的陣列工具類別,不過您覺得它的 setElement() 方法不夠安全,您希望增加一些陣列的長度檢查動作,於是您擴充該類別並重新定義 setElement() 方法: +```java public class SafeArray extends SimpleArray { public SafeArray(int i) { super(i); @@ -220,11 +223,14 @@ public class Cubic extends Rectangle { } .... } +``` 這麼一來,以 SafeArray 類別的定義所產生的物件,就可以使用新的定義方法,就 SafeArray 類別來說,由於操作介面與 SimpleArray 是一致的,所以您可以這麼使用: +```java SimpleArray simpleArray = new SafeArray(); simpleArray.setElement(); +``` SafeArray 與 SimpleArray 擁有一致的操作介面,因為 SafeArray 是 SimpleArray 型態的子類,擁有從父類中繼承下來的 setElement() 操作介面,雖然您使用 SimpleArray 型態的介面來操作 SafeArray 的實例,但由於實際運作的物件是 SafeArray 的實例,所以被呼叫執行的會是 SafeArray 中重新定義過的 setElement() 方法,這是「多型」(Polymorphism)操作的一個例子,8.2 中對多型操作還會再作說明。 @@ -232,12 +238,14 @@ SafeArray 與 SimpleArray 擁有一致的操作介面,因為 SafeArray 是 Sim 重新定義方法時要注意的是,您可以增大父類別中的方法權限,但不可以縮小父類別的方法權限,也就是若原來成員是 "public" 的話,您不可以在父類別中重新定義它為 "private" 或 "protected",所以在擴充 SimpleArray 時,您就不可以這麼作: +```java public class SafeArray extends SimpleArray { // 不可以縮小父類別中同名方法的權限 private void setElement(int i, int data) { .... } } +``` 嘗試將 setElement() 方法從 "public" 權限縮小至 "private" 權限是不行的,在進行編譯時編譯器會回報以下的錯誤訊息: @@ -248,6 +256,7 @@ SafeArray 與 SimpleArray 擁有一致的操作介面,因為 SafeArray 是 Sim 從 J2SE 5.0 開始在重新定義方法時,您可以重新定義返回值的型態,例如您原先設計了一個 Bird 類別: +```java public class Bird { protected String name; @@ -258,9 +267,11 @@ SafeArray 與 SimpleArray 擁有一致的操作介面,因為 SafeArray 是 Sim return new Bird(name); } } +``` getCopied() 方法原來返回的是Bird物件,現在打算擴充Bird類別,您繼承它並定義了一個 Chicken 類別,在 J2SE 5.0 之前,您會很苦惱於不能重新定義返回值型態,因此您勢必要重新寫一個方法名稱來傳回 Chicken 型態的返回值,但是從 J2SE 5.0 開始,重新定義返回值型態是可行的,重新定義返回值型態有限制條件,重新定義的返回值型態必須是父類別中同一方法返回型態的子類別,例如 Chicken 可以這麼定義: +```java public class Chicken extends Bird { protected String crest; @@ -273,6 +284,7 @@ getCopied() 方法原來返回的是Bird物件,現在打算擴充Bird類別, return new Chicken(name, crest); } } +``` 在 getCopied() 方法的返回值型態上,父類別中返回的是Bird型態,子類別在重新定義 getCopied() 方法時,可以重新定義一個 Bird 型態的子類別型態之返回值,在上面的程式片段中,所重新定義的返回值型態是 Chicken,它是 Bird 的子類別。 @@ -282,15 +294,19 @@ getCopied() 方法原來返回的是Bird物件,現在打算擴充Bird類別, 在 Java 中只要使用 "class" 關鍵字定義類別,您就開始使用繼承的機制了,因為在 Java 中所有的物件都擴充自 java.lang.Object 類別,Object 類別是 Java 程式中所有類別的父類別,每個類別都直接或間接繼承自 Object 類別,當您如下定義一個類別時: +```java public class Foo { // 實作 } +``` 在 Java 中定義類別時如果沒有指定要繼承的類別,則自動繼承 Object 類別,上面的程式片段即等於您如下定義類別: +```java public class Foo extends Object { // 實作 } +``` 由於 Object 類別是 Java 中所有類別的父類別,所以使用 Object 宣告的名稱,可以參考至任何的物件而不會發生任何錯誤,因為每一個物件都是 Object 的子物件,舉個簡單的例子,您可以製作一個簡單的集合(Collection)類別,並將一些自訂的類別之實例加入其中,範例 8.6 是個簡單示範。 @@ -393,7 +409,9 @@ finalize() 已經在 7.2.4 介紹過了;clone() 用於物件複製,稍後即 Object 的 toString() 方法是對物件的文字描述,它會返回 String 實例,您在定義物件之後可以重新定義 toString() 方法,為您的物件提供特定的文字描述,Object 的 toString() 預設會傳回類別名稱及 16 進位制的編碼,也就是傳回以下的字串: +```java getClass().getName() + '@' + Integer.toHexString(hashCode()) +``` getClass() 方法是 Object 中定義的方法,它會傳回物件於執行時期的 Class 實例, 再使用 Class 實例的 getName() 方法可以取得類別名稱;hashCode() 傳回該物件的「雜湊碼」(Hash code)。Object 的 toString() 方法預設在某些場合是有用的,例如 StringBuilder 就改寫了 toString() 方法,您可以呼叫 StringBuilder 實例的 toString() 方法以得到建構好的 String 實例(6.1.3 曾經介紹過 StringBuilder),範例 8.10是個簡單的示範。 @@ -614,18 +632,22 @@ public class CloneDemo { 回到多型操作的解釋上,現在假設 Class1 上定義了 doSomething() 方法,而 Class2 上也定義了 doSomething() 方法,而您定義了兩個 execute() 方法來分別操作 Class1 與 Class2 的實例: +```java public void execute(Class1 c1) { c1.doSomething(); } public void execute(Class2 c2) { c2.doSomething(); } +``` 很顯然的,您的程式中 execute() 分別依賴了 Class1 與 Class2 兩個類別,與其依賴兩個類別,不如定義一個父類別 ParentClass 類別,當中定義有 doSomething(),並讓 Class1 與 Class2 都繼承 ParentClass 類別並重新定義自己的 doSomething() 方法,如此您就可以將程式中的 execute() 改成: +```java public void execute(ParentClass c) { c.doSomething(); } +``` 這是可以行的通的,因為介面與實例上的操作方法是一致的,如圖 8.3 所示。 @@ -641,6 +663,7 @@ public class CloneDemo { 在 Java 中定義類別時,可以僅宣告方法名稱而不實作當中的邏輯,這樣的方法稱之為「抽象方法」(Abstract method),如果一個方法中包括了抽象方法,則該類別稱之為「抽象類別」(Abstract class),抽象類別是擁有未實作方法的類別,所以它不能被用來生成物件,它只能被繼承擴充,並於繼承後實作未完成的抽象方法,在 Java 中要宣告抽象方法與抽象類別,您要使用 "abstract" 關鍵字,以下舉個實際的例子,先假設您設計了兩個類別:ConcreteCircle 與 HollowCircle。 +```java public class ConcreteCircle { private double radius; public void setRedius(int radius) { this.radius = radius; } @@ -658,6 +681,7 @@ public class CloneDemo { System.out.printf("畫一個半徑 %f 的空心圓\n", getRadius()); } } +``` 顯然的,這兩個類別除了 render() 方法的實作內容不同之外,其它的定義是一樣的,而且這兩個類別所定義的顯然都是「圓」的一種類型,您可以定義一個抽象的 AbstractCircle 類別,將 ConcreteCircle 與 HollowCircle 中相同的行為與定義提取(Pull up)至抽象類別中,如範例 8.15 所示。 @@ -848,11 +872,13 @@ public class GuessGameDemo { 介面的宣告是使用 "interface" 關鍵字,宣告方式如下: +```java [public] interface 介面名稱 { 權限設定 傳回型態 方法(參數列); 權限設定 傳回型態 方法(參數列); // .... } +``` 在宣告介面時方法上的權限設定可以省略,如果省略的話,預設是 "public",來看宣告介面的一個實例。 @@ -934,23 +960,29 @@ public class RequestDemo { 在 Java 中您可以一次實作多個介面,實作多個介面的方式如下: +```java public class 類別名稱 implements 介面1, 介面2, 介面3 { // 介面實作 } +``` 當您實作多個介面時,記得必須實作每一個介面中所定義的方法,由於實作了多個介面,所以要操作物件時,必要時必須作「介面轉換」,如此程式才知道如何正確的操作物件,假設 someObject 實作了 ISomeInterface1 與 ISomeInterface2 兩個介面,則您可以如下對物件進行介面轉換與操作: +```java ISomeInterface1 obj1 = (ISomeInterface1) someObject; obj1.doSomeMethodOfISomeInterface1(); ISomeInterface2 obj2 = (ISomeInterface2) someObject; obj2.doSomeMethodOfISomeInterface2(); +``` 簡單的說,您每多實作一個介面,就要多遵守一個實作協議。介面也可以進行繼承的動作,同樣也是使用 "extends" 關鍵字來繼承父介面,例如: +```java public interface 名稱 extends 介面1, 介面2 { // ... } +``` 不同於類別一次只能繼承一個父類別,一個介面可以同時繼承多個父介面,實作子介面的類別必須將所有在父介面和子介面中定義的方法實作出來。 From 25b01a27fd02b9ed37c8c7a3e4f1acc02f6b291a Mon Sep 17 00:00:00 2001 From: "C.Y. Yang" Date: Sun, 12 Apr 2015 15:12:01 +0800 Subject: [PATCH 6/6] Update CH09.md --- docs/CH09.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/CH09.md b/docs/CH09.md index 29159f2..904a478 100644 --- a/docs/CH09.md +++ b/docs/CH09.md @@ -16,12 +16,14 @@ 先來介紹成員內部類別,基本上是在一個類別中直接宣告另一個類別,例如: +```java public class OuterClass { // 內部類別 private class InnerClass { // .... } } +``` 成員內部類別同樣也可以使用 "public"、"protected" 或 "private" 來修飾其存取權限,範例 9.1 簡單示範成員內部類別的使用。 @@ -91,12 +93,14 @@ public class PointShow { 內部類別還可以被宣告為 "static",不過由於是 "static",它不能存取外部類別的方法,而必須透過外部類別所生成的物件來進行呼叫,被宣告為 static的內部類別,事實上也可以看作是另一種名稱空間的管理方式,例如: +```java public class Outer { public static class Inner { .... } .... } +``` 您可以如以下的方式來使用 Inner 類別: @@ -110,9 +114,11 @@ public class PointShow { 內部匿名類別可以不宣告類別名稱,而使用 "new" 直接產生一個物件,內部匿名類別可以是繼承某個類別或是實作某個介面,內部匿名類別的宣告方式如下: +```java new [類別或介面()] { // 實作 } +``` 一個使用內部匿名類別的例子如範例7.3所示,您直接繼承 Object 類別定義一個匿名類別,重新定義 toString() 方法,並使用匿名類別直接產生一個物件。 @@ -138,6 +144,7 @@ public class AnonymousClassDemo { 注意如果要在內部匿名類別中使用外部的區域變數,變數在宣告時必須為 "final",例如下面的陳述是無法通過編譯的: +```java .... public void someMethod() { int x = 10; // 沒有宣告final @@ -150,6 +157,7 @@ public class AnonymousClassDemo { System.out.println(obj); } .... +``` 在進行編譯時,編譯器會回報以下的錯誤: @@ -157,6 +165,7 @@ public class AnonymousClassDemo { 您要在宣告區域變數x時加上"final"才可以通過編譯: +```java .... public void someMethod() { final int x = 10; // 宣告final @@ -169,6 +178,7 @@ public class AnonymousClassDemo { System.out.println(obj); } .... +``` 為什麼要加上 "final" 宣告呢?原因在於區域變數 x 並不是真正被拿來於內部匿名類別中使用,x 會被匿名類別複製作為資料成員來使用,由於真正在匿名類別中的x是複製品,即使您在內部匿名類別中對 x 作了修改(例如 x=10 的指定),也不會影響真正的區域變數x,事實上您也通不過編譯器的檢查,因為編譯器會要求您加上 "final" 關鍵字,這樣您就知道不能在內部匿名類別中改變 x 的值,因為即使能改變也沒有意義。 @@ -359,6 +369,7 @@ public class Point2DDemo3 { 類別上的權限設定會約束類別成員上的權限設定,所以如果類別上不宣告 "public",而類別成員上設定了 "public",則類別成員同樣的也只能被同一個套件的類別存取,也就是說如果您這麼撰寫程式: +```java package onlyfun.caterpillar; class SomeClass { // ... @@ -366,9 +377,11 @@ public class Point2DDemo3 { // .... } } +``` 其效果等同於: +```java package onlyfun.caterpillar; class SomeClass { // ... @@ -376,25 +389,31 @@ public class Point2DDemo3 { // .... } } +``` 由這邊的討論,可以再來看看預設建構方法的權限。首先要知道的是,當您在 Java 中定義一個類別,但沒有定義建構方法時,編譯器會自動幫您產生一個預設建構方法,也就是說,如果您這麼寫: +```java package onlyfun.caterpillar; public class Test { .... } +``` 則編譯器會自動加上預設建構方法,也就是相當於這麼寫: +```java package onlyfun.caterpillar; public class Test { public Test() { } .... } +``` 如果您自行定義建構方法,則編譯器就不會幫您加上預設建構方法,所以當您這麼定義時: +```java package onlyfun.caterpillar; public class Test { public Test(int i) { @@ -402,13 +421,17 @@ public class Point2DDemo3 { } .... } +``` 則在建構時,就必須指明使用哪個建構方法,簡單的說,您就不能使用以下的方式來建構: +```java Test test = new Test(); +``` 有時會建議即使沒有用到,在定義自己的建構方法的同時,也加上個沒有參數的建構方法,例如: +```java package onlyfun.caterpillar; public class Test { public Test() { // 即使沒用到,也先建立一個空的建構方法 @@ -419,28 +442,35 @@ public class Point2DDemo3 { } .... } +``` 要注意的是,在繼承時,如果您沒有使用 super() 指定要使用父類別的哪個建構方法,則預設會尋找父類別中無參數的建構方法。 預設建構方法的存取權限是跟隨著類別的存取權限而設定,例如: +```java package onlyfun.caterpillar; public class Test { } +``` 由於類別宣告為 public,所以預設建構方法存取權限為 public。如果是以下的話: +```java package onlyfun.caterpillar; class Test { } +``` 則預設建構方法存取權限為套件存取權限,也就是編譯器會自動為您擴展為: +```java package onlyfun.caterpillar; class Test { Test() { } } +``` 在這邊整理一下 private、protected、public 與 default 與類別及套件的存取關係: @@ -515,15 +545,18 @@ public class ImportStaticDemo { 如果編譯器無法判斷,則會回報錯誤,例如若您定義了如下的類別: +```java package onlyfun.caterpillar; public class Arrays { public static void sort(int[] arr) { // .... } } +``` 然後如下撰寫程式: +```java import static java.lang.System.out; import static java.util.Arrays.sort; import static onlyfun.caterpillar.Arrays.sort; @@ -536,6 +569,7 @@ public class ImportStaticDemo { } } } +``` 由於從 java.util.Arrays.sort 與 onlyfun.caterpillar.Arrays.sort 的兩行 "import static" 上都可以找到 sort,編譯器無法辦別要使用哪一個 sort() 方法,因而編譯時會出現以下的錯誤: