--- title: Java CH4 tags: Java, OCA, OCP, SE8, CH4 Date: 2019/11/18 2122 --- ## Ch4. 類別 (Class) ### 物件導向 1. 精神 * 抽象化 (Abstraction) * 有助於了解並實際掌握物件的實際內容 * 繼承 (Inheritance) * 賦予程式能夠重複使用 2. 特徵 * 繼承 (Inheritance) * 封裝 (Encapsulation) * 多型 (Polymorphism) ### 類別 > 是物件的藍圖,物件實體的基礎 > 其內部有使用者定義的資料型態:屬性 (Attribute) 和方法 (Method) > 類別的宣告需使用**class**關鍵字 [color=#FFAAFF] ``` 存取修飾子 修飾語 class 類別名稱 { 屬性1; 屬性2; ... 方法1; 方法2; ... } ``` - 存取修飾子:public protected (default) private - 若類別存取修飾子是**public**,則該檔名和類別名稱一定要相同!! #### 物件屬性 > 不加**static**來修飾的變數 > 相反的叫做**類別變數**,又叫做**靜態變數** [color=#FF0000] ``` public class A{ int i = 10; //物件變數 static int b = 0; //類別變數 } ``` - 差別 - 物件變數:由各個物件獨自維護(存取) - 類別變數:所有從該類別實作出來的物件都可以存取(共享) - 假設從上面的類別A實作出了物件a, b, c,討論其內的變數關係 ![](https://i.imgur.com/p31WUPd.png) #### 物件方法 > 通常物件不允許外部直接存取類部的資料,而方法是作為兩者之間的橋樑 > 跟物件屬性一樣,不加**static**為物件方法 > 相反的叫做**類別方法**,又叫做**靜態方法** > 不管是類別方法還是物件方法都不能再方法內再宣告一個方法 > [color=#FF0000] ``` public class A{ void methodA(){} //物件方法 static void methodB(){} //類別方法 void methodC(){ //void methodD(){} //編譯錯誤 } } ``` #### 建立物件實體 (Instance) - 方法一 - `類別名稱 物件名稱 = new 類別名稱();` - `A a = new A();` - 方法二 ``` 類別名稱 物件名稱; 物件名稱 = new 類別名稱(); A a; a = new A(); ``` ![](https://i.imgur.com/340QLQc.png) - 最後透過物件變數 a 來操作 new A() 物件實體 #### 存取調用屬性和方法 - `變數名稱.屬性;` - `變數名稱.方法();` - 注意 - 靜態成員**不能**存取非靜態成員 - 但是非靜態成員**可以**存取靜態成員 ( **不推薦這樣寫** ) ##### 範例 ``` public class ex1{ public static void main(String[] args){ A a = new A(); System.out.println(a.i); //12 System.out.println(a.j); //134 (不推薦) System.out.println(A.j); //134 a.mA(); //12 \n 134 a.mB(); //134 (不推薦) A.mB(); //134 } } class A{ int i=12; static int j=134; void mA(){ System.out.println(i); System.out.println(j); } static void mB(){ System.out.println(j); } } ``` ### 建構子 (Constructor) > 在建立物件時,會在記憶體配置一個區塊來存放物件,同時會執行建構子函式。 > 建構子是第一個執行的函式,通常是用來初始化物件 - 特性:**沒有回傳值**、**名稱必須與類別名稱相同** - 語法:存取權限 類別名稱 ( 參數列 ) ``` public class Test{ public static void main(String[] args){ A a = new A(); A a1 = new A(8); System.out.println("\n----------分隔線---------\n"); a.A(); } } class A{ A(){ //一般無參數的建構子 System.out.println("執行A的建構子A()") } A(int i){ //含有參數的建構子 System.out.println("執行A(int i)建構子,傳入參數:" + i); } void A(){ System.out.println("執行void A(),雖然與類別同名,"); SYstem.out.println("但是有void,對A類別而言,只是一個普通的方法"); } } ``` - 輸出: - 執行A的建構子A() - 執行A(int i)建構子,傳入參數:8 - \n----------分隔線---------\n - 執行void A(),雖然與類別同名, - 但是有void,對A類別而言,只是一個普通的方法 #### 預設建構子 (Default Constructor) - 每個 class 最多有一個預設建構子 - 若程式碼中**沒有定義任何的建構子**,編譯時期會自動加入預設建構子 [color= #FF0000] - 預設建構子和該類別存取權限相同 - **沒有參數列** - 除了初始成員變數或繼承時 super()的定義外,基本上是沒有其他敘述的 ``` public class TestA{ int k; } public class TestB{ int k=10; } 經過編譯後,反組譯... public class TestA{ int k; public TestA(){ } } public class TestB{ int k; public TestB(){ k = 10; } } ``` ### static 成員與 non-static 成員 > 怎麼分?? > 看那個屬性或方法前面有沒有 **static** 的關鍵字 - 分類 - static:類別成員,包含靜態內部類別、類別屬性、類別方法 - non-static:物件成員,包含物件屬性、物件方法 - 規則 - non-static 可以存取 non-static 和 static 成員 - static 不行存取 non-static 成員 - 原因 - static 是屬於類別成員,不必建立物件就可以存取,所以呼叫時,不會傳入物件的參考位置 (this 的參考)。 - 相對的,non-static 成員必須先建立物件(透過 this 的參考)才能對其下成員進行存取。 ``` class Test{ void mA(){ mB(); //non-static call non-static mD(); //non-static call static } void mb(){} static void mC(){ mB(); //static call non-static,failure (Compile-time error) mD(); //static call static } static void mD(){ new.Test().mB(); //instance an object then call non-static } } ``` ### 覆寫 (Overriding) - 發生在繼承關係 - 是由子類別自行實作父類別的方法 (取代) - 而實際執行會執行子類別的方法,而不執行父類別方法 ``` class Father{ void mA(){ System.out.println("Father's mA()"); } } class Son extends Father{ @Override void mA(){ System.out.println("Son's mA()"); } } public class Test{ public static void main(String[] args){ Son s = new Son(); s.mA(); } } 輸出:Son's mA() ``` - 特徵: - 發生在子類別的方法,要實作父類別的方法(覆寫) - 回傳型別 方法名稱(參數列) **須完全相同** - 參數列的順序也要相同 - 習慣加 **@Override**在覆寫的方法前註記 (最好加上) - 存取權限不可以小於原方法 - 父類別方法有拋出例外時,子類別可以選擇是否要拋出例外 - 與原方法相同 - 原方法例外事件的子類別 #### 有例外的覆寫 - 該方法修飾的例外事件必須包含原方法所修飾的例外事件內 - 不丟 - 丟例外的話 - 或是同樣的類別 - 子類別的例外要是父類別例外的子類別 ``` class F{ void m() throws Exception {} } class S extends F{ @Override void m() throws IOException, Exception {} } ``` - 在根據例外種類,視情況加上 try...catch,或 throws 丟出 ``` import java.io.*; class Father{ void mA() throws Exception{} } public class Son extends Father{ public static void main(String[] args){ Son s = new Son(); try{ //看例外種類視情況加try...catch s.mA(); }catch(IOException e){} } @Override void mA() throws IOException{} //可以不丟例外 } ``` #### Override 回傳型別 #### 遮蔽 - 屬性的也有覆寫,稱為『遮蔽』 - 只是單純的覆蓋父類別屬性,但可以用 super.a 存取父類別屬性 ``` class F{ int a; } class S extends F{ int a = 10; } 上面的F裡的a,被S的a遮蔽 ``` ### 超載 (Overloading) #### 定義 - 只同一種方法,可以產生不同的實作 #### 特徵 - 方法名稱必需一樣 - 欲傳入的參數列不能相同,但順序顛倒不在此限 - 基於前一個特徵,回傳型別可以不一樣 ``` class Father{ void mA(){} // int mA(){} 跟上一列衝突,造成方法重複定義 float mA(int a){} //超載 void mA() int mB(String a, int b){} } class Son extends Father{ int mB(int b, String a){} // 超載Father的mB() } ``` ### vararg 建構變長的參數 - 假設我們要傳一個陣列到方法裡,求加總 ... ``` int plus(int[] a){ int sum=0; for(int a:x){ sum += x; } return sum; } 我們須在外面先建好一個陣列,然後傳進來求和 看似簡單,但... 不覺得有點麻煩?? ``` ``` int plus(int ... a){ int sum=0; for(int c:x){ sum += x; } return sum; } 嗯??你說我只是換個不同的參數列而已 (可惡騙我! 不不不 ~ 有差異的地方是在呼叫端,你看... => 用 int plus(int[] a) 的 plus(new int[]{1,1,2,3,4}); plus(new int[]{1,2,3}); => 用 int plus(int... a) 的 int a = plus(1,2,3,4); int b = plus(9,4,2); int c = plus(a,b); 看出差異了吧? ``` #### 檢視反組譯後 int plus(int... a) ``` transient int (int c[]){ ... } ``` - 編譯器將 int... a 轉成 int a[] - 在方法前加上 transient - 作為成員變數的修飾子時,不會序列化該屬性的內容值 - 在編譯階段不能拿來修飾成員方法 - 但在轉譯成 class 檔時,編譯器會自動判斷是否要加 #### (...)的出現位置與限制 - 須放在參數列的最後一個位子 ``` void plus(int a, int... b){} void plus(int... b, int a){} //錯誤,須放在參數列的最後一個位子 ``` - 至多只能有一個 ``` void plus(int a, int... b){} void plus(int... b, int... b){} //錯誤,至多只能有一個 ``` - 若參數列只有它時,則呼叫端不傳參數進來,也是被允許的 ### 封裝 (Encapsulation) > 透過統一方法或介面實作來取得那些類別不允許被外部直接存取的內部資料,以維護物件資料的完整性和存取安全 > 簡單來說,就是透過 public, protected, (default), private 來封裝類別內的屬性及資料 ``` public class Test{ public static void main(String[] args){ A a = new A(); a.setMoney(1000); System.out.println(a.getMoney()); } } class A{ private int money; //因為 private 不能直接存取,透過公開的方法來修改他 //public int money; 假如 a.setMoney(1000); 改成 a.money = -3392; 呢?? 是不是覺得有點恐怖?! (這就是封裝的好處 public void setMoney(int m){ this.money = m; } public int getMoney(){ return this.money; } } ``` ### 繼承 ( Inheritance ) > 在 java 中只允許單一繼承關係,也就是一個子類別只能有父類別,不能使用多個父類別【多重繼承】。 > 用生活中的例子來看,一個孩子只會有一個父親,但父親卻可以有很多個孩子。 > 但是可以用介面實作 ( implements interface ) 的方法,來模擬多重繼承的效果 #### 一般 - 語法:子類別 extends 父類別 - 注意:在 Java 的機制裡,子類別是包含著父類別的,所以子類別看的到父類別的東西,相對的,父類別卻不能看到子類別的東西。 ``` class Father{ public int num = 1000; public void show(){ System.out.println(num); } } class Son extends Father{} public class Test{ public static void main(String args[]){ Son s = new Son(); s.show(); System.out.println(s.num); } } ``` #### 繼承下的建構子 > 先看下面的程式碼 ... ``` class Father{ Father(){ System.out.println("Father"); } } class Son extends Father{ Son(){ System.out.println("Son"); } } ``` 1. 因 Son 繼承 Father,javac 會自動加上 ... ``` class Son extends Father{ Son(){ super(); //呼叫父類別不帶參數的建構子 ... } } ``` 2. 輸出 ``` Father Son ``` - 注意: - 若 Father 的建構子改成有帶參數的情況,其他保持不變 - 則會產生編譯時期錯誤,『找不到Father(){}的建構子』 - 因為根本沒有寫上去 ... - 解法有 2 種 - 在 Father 裡,補上一個不帶參數的建構子 - 在 Son 裡,補上【super(對應的參數);】的程式碼 #### this 和 super - this 是用來呼叫自己類別裡的資料 - super 是用來呼叫父類別裡的資料 - 若寫在建構子或函式裡,this()、super() 都必須寫在第一行,但只能擇一 - this 和 super 不能呼叫 static 成員 ### 多形 ( Polymorphism ) > 多形式為了開發出可擴充的程式,使撰寫上更有彈性。 - 泛指在具有繼承關係的結構下,單一物件實體能被宣告成多種型別。 ``` class Animal{} class Bird extends Animal{} class Cat extends Animal{} class Tiger extends Cat{} ``` - 宣告變數來調用 new Tiger() 時,有 3 種情況: - `Tiger t = new Tiger();` - 以老虎的觀點看老虎 - `Cat c = new Tiger();` - 以貓科的觀點看老虎 - `Animal a = new Tiger();` - 以動物的觀點看老虎 - 這個例子會造成編譯錯誤 - `Tiger t = new Cat();` - 以老虎的觀點看貓科? - 將所有的貓科動物都看成老虎? - 以技術的觀點來看: - Tiger 是子類別,Cat 是父類別 - 而子類可視範圍大於父類,實體的是 Cat,總不可能會有 Tiger 特有的方法吧?? - 再用上面的程式碼做變化 ``` class Animal{ void move(){ System.out.println("move..."); } } class Bird extends Animal{ void move(){ System.out.println("fly..."); } } class Cat extends Animal{ void move(){ System.out.println("run..."); } void skill(){ System.out.println("shower..."); } } class Tiger extends Cat{ void skill(){ System.out.println("hunt..."); } } public class Zoo{ public static void main(String args[]){ Tiger t = new Tiger(); t.skill(); t.move(); System.out.println("---------------"); Cat c = new Tiger(); c.skill(); c.move(); System.out.println("---------------"); Animal a = new Tiger(); a.move(); a.skill(); } } ``` - 輸出: ``` hunt... //t 用 Tiger的觀點去操作 new Tiger()實體的 skill() run... //因繼承Cat,透過覆寫機制,實作了自己的move() --------------- hunt... //因為實際操作的是 new tiger()實體 run... //同理,再透過覆寫機制 --------------- run... //因為實際操作的是 new tiger()實體 [編譯錯誤!!] //因為Animal裡沒有skill(),所以不允許呼叫 ``` - 問題:怎麼解決因型別不同而不能呼叫某個方法呢?? - 運用強制轉型 - 語法:`(欲轉型的物件型別)(物件實體)` 或是 `(欲轉型的物件型別)物件變數` - EX: - (Tiger)(new Animal()) - ((Tiger)a) - 將前面的程式碼改成這個樣子 ... ``` public class Zoo{ public static void main(String args[]){ Animal a = new Tiger(); a.move(); ((Tiger)a).skill(); //a從Animal轉成Tiger型別 } } ``` - 就能以Tiger的觀點去執行.skill()的功能了 --- - 在考慮下面的程式碼 ... ``` public class Zoo{ public static void main(String args[]){ Animal a = new Tiger(); a.move(); ((Bird)a).skill(); //a從Bird轉成Tiger型別 } } ``` - 雖然編譯時期會過,但是執行時期會產生 **java.lang.ClassCastException**的錯誤 (物件型別轉換的例外) - 因為編譯器會把 Bird 視為 Animal 下的子類別,但這是不符合邏輯的。 - 這時有個運算子來幫我們判別是否符合邏輯的物件傳換 - 語法: `物件名稱 instanceof 物件名稱` - 回傳值:true / false - True:代表該物件該物件變數有參考到該類別的物件實體,可以轉型 - False:反之 #### 多形與屬性 ``` class F{ String name = "Father"; String getName(){ return name; } String saysth(){ return "F"; } } class S extends F{ String name = "Son"; String saysth(){ return "S"; } } public class poly{ public static void main(String args[]){ //codes... } } ``` - 先改成下面這樣,確定基礎 ``` F f = new F(); out.print(f.saysth() + ", "); out.print(f.name + ", "); out.println(f.getName()); //結果: F, Father, Father ``` - 那改成下面這樣? ``` F f = new S(); out.print(f.saysth() + ", "); //透過覆寫機制 out.print(f.name + ", "); //請看成F型別下的name,所以是Father out.println(f.getName()); //透過繼承到F的getName(),回傳F型別下的name,所以是Father //結果: S, Father, Father ``` #### 多型在方法上的應用 ``` class Animal { void action(){ System.out.println("action...); } } class Bird extends Animal{ void action(){ System.out.println("flying...); } } class Cat extends Animal{ void action(){ System.out.println("running...); } } class Tiger extends Animal{ void action(){ System.out.println("hunting...); } } public class Zoo { public static void main(String[] args) { //假設我們要查動物的行為 Zoo zoo=new Zoo(); zoo.getAction(new Animal()); zoo.getAction(new Bird()); zoo.getAction(new Cat()); //...etc } void getAction(Animal animal){ animal.action(); } void getAction(Bird bird){ bird.action(); } ... etc 或是,只寫一個就搞定?! void getAction(Animal animal){ animal.action(); //這個就是多型的好處與優勢 } } ``` ### is-a 和 has-a 術語 - is-a:是一種上下關係 - Human extend Animal - we called Human is-a Animal - has-a:是一種聚合關係 - Computer has-a cpu, ram, vga... - 用來表示類別 has-a 某某成員變數 ### Interface(介面) - 設定完後,不允許更動,否則失去意義 - 可以用來收集多個類別,而這些類別可以不用建立任何關係 - 語法:```存取權限修飾子 interface 介面名稱 [+ extends ...]``` - 存取權限修飾子與一般的class一樣,只能是default或public,但在內部類別裡時,並無限制。 - 內定的屬性必須要給定初始值,方法不能有實作 - ``` interface test { String a = "asd"; => public static final a = "asd"; void method(); => public abstract method(); } ``` - 介面不能自行實作(不能直接用new建立物件) - 用匿名類別 - 用別的類別實作(implements)介面 - 幫她實作的類別必須要把該介面的所有方法實作出來 - 否則該類別必須變成抽象類別 - 實作的過程採用覆寫機制 - Java沒有『多重繼承』,但可以透過下面的方式來達成這個效果。 - 介面繼承多個介面 - 類別實作多個介面 - class 和 interface - [interface] implements [interface2], [interface3] - [class] implements [interface] - [class] extends [class] implements [interface1], [interface2] ### Abstract Class (抽象類別) - 為了讓方法由子類別來決定,使其使用更多樣化 - 類似介面實作的概念,若沒有全部覆寫實作出來所有的抽象方法,那該類別需宣告成抽象類別 - 與介面不同地方 - 抽象方法可以有可無 (不能有static) - ```[public protected (default)] abstract void method(args...);``` - 不能是private - 屬性可以只有宣告 - 相同地方 - 不能自行建立實體物件,跟介面建立物件觀念一樣 - 語法:``` 存取修飾子 abstract class 類別名稱 [extends ...] [implements ...] ``` ### Enum (列舉) > 需要有人補充 >