第二章 面向對象java
這章開頭說,「做爲一名SCJP6,意味着你必須精通Java中面向對象的知識。必須熟悉繼承層次結構,靈活自如地應用多態性的強大能力,內聚與鬆散耦合必須成爲你的第二性徵,複合則成爲你的謀生之道。」(複合類型就是引用類型,包括類引用、接口引用、數組引用)。數據庫
在現在框架橫飛的年代,咱們就像拼裝工人同樣,不問爲何,不去思考,沒有思想。我經常對本身說,我不要再寫垃圾代碼了。但老是昨夜還沉浸在《Java與模式》的優雅,次日就又屈服於疲於奔命的進度和莫名其妙的設計文檔了。編程
每一個夜晚你總會在寫字樓的6層看見一個爲編程癡狂的老少子,一邊敲着鍵盤,一邊搖頭晃腦的吼着汪峯的《我想要盛開的生命》,加油!數組
2.1 封裝架構
考試目標5.1 編寫代碼,實現類中的緊封裝、鬆耦合和高內聚,並描述這樣作的優勢。框架
爲何要封裝?ide
一般在類中,咱們的實例變量(定義在類中,但位於任何方法以外,而且只有在實例化類時纔會被初始化的變量),還有一些只有本類會用到的方法,都用private來聲明,而後若是須要對實例變量訪問,就寫一些getter和setter。函數
若是都public了呢?好比你定義了一條記錄的ID是用sequence獲取的,一個調用者本身指定了一箇舊的ID給對象,作插入數據庫的操做,那必定是要拋異常了。測試
如何實現封裝:動畫
- 保護實例變量(使用訪問修飾符,一般是private)
- 創建public訪問器方法,強制調用代碼使用這些方法而不是直接訪問實例變量。
- 對於訪問器方法,使用JavaBeans命名規則set<propertyName>和get<propertyName>
2.2 繼承、IS-A、HAS-A關係
考試目標5.5 編寫代碼,實現IS-A關係和/或HAS-A關係。
書雲:「不使用繼承,即使編譯最微小的Java程序也幾乎是不可能的。」,繼承能夠說是面向對象的基礎。
子類繼承超類,子類繼承了超類的非私有的成員變量和成員方法,就像這些成員原本就是他們本身的同樣。
須要注意的是Java不支持多重繼承,一個類只能直接繼承一個類。
繼承的做用:
- 促進代碼的複用。這個很好理解,全部的類都繼承自Object,它提供給全部的類equal()等方法,若是全部的類都要本身實現這個方法的話,那太可怕了。
- 使用多態性。書中給了個載入遊戲圖形的例子:
//遊戲圖形的超類,全部子類經過繼承GameShape來得到顯示圖形的方法 class GameShape { //顯示圖形的方法 public void displayShape(){ System.out.println("displaying shape"); } } //GameShape的一個子類,遊戲人物的圖形對象 class PlayerPiece extends GameShape{ //code } //GameShape的一個子類,牆磚的圖形對象 class TilePiece extends GameShape{ //code } //如今假設有一個GameLauncher類,當咱們進入這張地圖的時候,它會把這些圖形對象(即GameShape的子類)都載入進來。 //換句話說,GameLauncher的工做就是實例化這些XxxxPiece類,而後讓他們調用父類GameShape的displayShape()方法。 class GameLauncher{ //這個方法並不關心參數是GameShape的哪一個子類。 public static void doShapes(GameShape shape){ shape.displayShape(); } public static void main(String[] str){ PlayerPiece player = new PlayerPiece(); TilePiece tile = new TilePiece(); doShapes(player); //體現了多態性的好處,加入後面又加入了新的Piece, doShapes(tile); //好比WeaponPiece,在doShapes中依然不用關心它是什麼。 } }
2.2.1 IS-A關係
在OO中,IS-A的概念基於類繼承和接口實現。在Java中,使用extends和implements來表達IS-A關係。
IS-A:書雲「這個東西是那個東西的一種」。我以爲這個解釋足夠了,再也不將這個概念妖魔化了。= =
類A繼承類B,能夠說「類A IS-A 類B」.
2.2.2 HAS-A關係
HAS-A關係基於引用。類A中的代碼具備對類B實例的引用,則「類A HAS-A 類B」。
書雲:「IS-A、HAS-A關係以及封裝只是面向對象設計的冰山一角」。其實咱們在設計架構的時候,就是從這些基本的概念出發的。記得和同事在設計類的時候,一個同事說:「是它的就是它的,不可分割的就給它;不是它的就不是它的,不能生加上去。」這是一個原則,可是我也反對爲了面向對象而面向對象。
2.3 多態性
考試目標5.2 給定一個場景,編寫代碼,演示多態性的使用。並且,要判斷什麼時候須要強制轉化,還要區分與對象引用強制轉換相關的編譯器錯誤和運行時錯誤。
多態性:能夠傳遞多個IS-A測試的任何Java對象均可以被看做是多態的。
訪問對象的惟一方式是經過引用變量。關於引用,要記住:
- 引用變量只能屬於一種類型。一經聲明,類型就永遠不能再改變(儘管它引用的對象能夠改變類型)。
- 引用是一個變量,所以它能夠從新賦予給其餘對象(除非該引用被聲明爲final)。
- 引用變量的類型決定了能夠在該變量引用的對象上調用的方法。
- 引用變量能夠引用具備與所聲明引用的類型相同的任何對象,或者——最重要的一點是——它能夠引用所聲明類型的任何子類型。
- 引用變量能夠聲明爲類類型或接口類型。若是將變量聲明爲接口類型,它就能夠引用實現該接口的任何類的任何對象。
前面2.2咱們說到「在OO中,IS-A的概念基於類繼承和接口實現」,2.2中基於類繼承的說的比較多,接口一樣能夠表達IS-A的關係,實現多態。
Java爲何沒有多重繼承?
若是一個類擴展另外兩個類,而且兩個超類都具備doStuff()方法,那麼問題就出險了:子類將繼承doStuff()方法的哪一個版本訥?這個問題可能致使一種「致命的死亡菱形」的情形,B、C繼承A,D繼承B、C,並且B、C都重寫類A中的一個方法,那麼從理論上講,類D就繼承了同一個方法的兩種不一樣實現。Java的設計者考慮到這種可能的混亂,因此規定一個類只能直接繼承一個超類。
那若是我有下面的需求該怎麼辦呢?
好比上面的GameShape例子,它的子類經過繼承它,得到了displayShape()方法,來顯示圖像。如今我想讓GameShape的子類均可以使用Animatable類的animate()方法,來實現遊戲貼圖的一些動畫。
因爲animate()方法不只提供給GameShape的子類,也會被其餘的類使用,好比Game2Shape,因此我不能把animate()寫在GameShape中。但又不能繼承兩個類,而在每一個子類中都寫一個本身的animate()顯然又不優雅。
這時,咱們就能夠用接口來實現這個需求。即
interface Animatable{ void animate(); } class PlayerPiece extends GameShape implements Animatable{ public void animate(){ //code } }
這就完成了用接口實現IS-A關係。
以PlayerPiece爲例,咱們能夠說
- PlayerPiece IS-A Object
- PlayerPiece IS-A GameShape
- PlayerPiece IS-A PlayerPiece
- PlayerPiece IS-A Animatable
以上就體現了PlayerPiece的多態性。
多態方法調用僅使用於實例方法。不涉及靜態方法和變量。
而在繼承中子類可使用超類的方法,也能夠本身來實現這一方法,這就涉及到了重寫(override)。
2.4 重寫和重載
考試目標1.5 給定一個代碼示例,判斷一個方法是否正確地重寫或重載了另外一個方法,並判斷該方法的合法返回值(包括協變式返回值)。
考試目標5.4 給定一個場景,編寫代碼,聲明和/或調用重寫方法或重載方法。編寫代碼,聲明和/或調用超類、重寫構造函數或重載構造函數。
2.4.1 重寫方法(override)
重寫的規則:
- 變元列表必須與被重寫的方法的變元列表徹底匹配。
- 返回類型必須與超類中被重寫方法中原先聲明的返回類型或其子類型相同。
- 訪問級別的限制性必定不能比被重寫方法的更嚴格。
- 訪問級別的限制性能夠比被重寫方法的弱。
- 僅當實例方法被子類繼承時,它們才能被重寫。與實例的超類同包的子類能夠重寫未標識爲private或final的任何超類方法。不一樣包的子類只能重寫那些標識爲public或protected的非final方法。
- 重寫方法能夠拋出任何未檢驗(運行時)異常,不管被重寫方法是否聲明瞭該異常。
- 重寫方法必定不能拋出比被重寫方法聲明的檢驗異常更新或更廣的檢驗異常。好比,一個聲明FileNotFoundException異常的方法不能被一個聲明SQLException、Exception或任何其餘非運行時異常的方法重寫,除非它是FileNotFoundException的一個子類。
- 重寫方法可以拋出更少或更有限的異常。
- 不能重寫表示爲final的方法。
- 不能重寫標識爲static的方法。
調用被重寫方法的超類版本:super關鍵字
2.4.2 重載方法(overload)
重載的規則:
- 重載方法必須改變變元列表。
- 重載方法能夠改變返回類型。
- 重載方法能夠改變訪問修飾符。
- 重載方法能夠聲明新的或更廣的檢驗異常。
- 方法可以在同一個類或者一個子類中被重載。
調用重載方法:調用哪一個重載方法,取決於變元的類型。
class Animal {}
class Horse extends Animal{}
class UseAnimals{
public void doStuff(Animal a){
System.out.print("In the Animal version");
}
public void doStuff(Horse h){
System.out.print("In the Horse version");
}
public static void main(String[] str){
UseAnimals ua = new UseAnimals();
Animal obj = new Horse();
ua.doStuff(obj); //在這裏引用類型決定了調用哪一個重載方法
}
}
//結果顯示"In the Animal version"
重載方法和重寫方法中的多態性
用一個例子來講明:
public class Animal { public void eat(){ System.out.println("Generic Animal Eating Generically"); } } public class Horse extends Animal{ public void eat(){ System.out.print("Horse eating hay"); } public void eat(String s){ System.out.print("Horse eating "+s); } } //測試方法 public class Test{ public static void main(String[] str){ //這裏是下表中「方法調用的代碼」 } }
不一樣調用方法的結果:
方法調用的代碼 | 結果 | 解釋 |
Animal a = new Animal(); a.eat(); |
Generic Animal Eating Generically | |
Horse h = new Horse(); h.eat(); |
Horse eating hay | |
Animal ah = new Horse(); ah.eat(); |
Horse eating hay | 多態性起做用——肯定調用的是哪一個eat()時,使用的是實際的對象類型(Horse),而不是引用類型(Animal) 注意和前一個例子的狀況相區分,前面的狀況是選擇用UseAnimals對象的哪一個方法,由變元類型(或說引用類型)來決定(編譯時); 如今的狀況是選擇用哪一個對象的eat()方法,Animal裏若是沒有這個方法會報編譯時錯誤,可是就算有,運行時仍是由實際的對象類型來決定。 |
Horse he = new Horse(); he.eat("Apples"); |
Horse eating Apples | 調用重載方法eat(String s); |
Animal a2 = new Animal(); a2.eat("treats"); |
編譯時錯誤 | Animal沒有帶String變元的eat()方法 |
Animal ah2 = new Horse(); ah2.eat("Carrots"); |
編譯時錯誤 | 緣由同上 |
重載方法和重寫方法的區別:
重載方法 | 重寫方法 | |
變元 |
必須改變 | 必定不能改變 |
返回類型 | 能夠改變 | 除協變式返回外,不能改變 |
異常 | 能夠改變 | 能夠減少或消除。必定不能拋出新的或更廣的檢驗異常 |
訪問級別 | 能夠改變 | 必定不能執行更嚴格的限制(能夠下降限制) |
調用 | 引用類型決定了選擇哪一個重載版本(基於聲明的變元類型)。在編譯時刻作出決定。 調用的實際方法仍然是一個在運行時發生的虛擬方法調用,可是編譯器老是知道所調 用方法的簽名。所以在運行時,不只是方法所在的類,並且變元匹配也已經明確了。 |
對象類型(也就是堆上實際的實例的類型)決定了調用哪一個方法。 在運行時決定。 |
2.5 引用變量強制轉換
考試目標5.2 給定一個場景,編寫代碼,演示多態性的使用。並且,要判斷什麼時候須要強制轉化,還要區分與對象引用強制轉換相關的編譯器錯誤和運行時錯誤。
向下轉型:把引用變量轉換爲子類類型。如Horse h = (Horse) new Animal();但若是調用父類裏沒有的方法,能夠經過編譯,但運行時會拋出java.lang.ClassCastException異常。
向上轉型:把引用變量轉換爲超類類型。如Animal a = new Horse(); 不須要轉化,這是自然的IS-A 關係。
2.6 實現接口
考試目標1.2 編寫代碼,聲明接口。編寫代碼,實現或擴展一個或多個接口。編寫代碼,聲明抽象類。編寫代碼,擴展抽象類。
在第一章的「聲明接口」裏說過,接口就是一種契約,任何實現這個接口的實現類都必須贊成爲該接口的全部方法提供實現。
合法的非抽象實現類必須執行如下操做:
- 爲來自所聲明接口的全部方法提供具體(非抽象)的實現。
- 遵照合法重寫的全部規則。
- 在實現方法上聲明非檢驗異常,而不是在接口方法上聲明,也不是在接口方法上什麼異常的子類。
- 保持接口方法的簽名,而且保持相同的返回類型(或子類型),可是沒必要聲明在接口方法聲明中聲明過的異常。
兩條規則:
- 一個類能夠實現多個接口。書雲:「子類將定義你是誰以及是作什麼的,而實現則定義你所扮演的角色或者你能戴的帽子,而不會理會你與實現一樣接口(但來自不一樣的繼承樹)的其它類有多大的差異」。
- 接口自身可繼承另外一個接口,並且接口能夠繼承多個接口。
2.7 合法的返回類型
考試目標1.5 給定一個代碼示例,判斷一個方法是否正確地重寫或重載了另外一個方法,並判斷該方法的合法返回值(包括協變式返回值)。
2.7.1 返回類型的聲明
哪些內容聲明爲返回類型,這主要取決因而在重寫方法、重載方法仍是在聲明新方法。
重載方法上的返回類型
沒有什麼限制,重載方法關鍵是變元要變化。
重寫、返回類型和協變式返回
從Java5開始,只要新的返回類型是被重寫的(超類)方法所聲明的返回類型的子類型,就容許更改重寫方法中的返回類型(這就是傳說中的協變式返回)。之前的Java版本要求重寫的方法返回類型必定要與原來的一致。
2.7.2 返回值
六條規則:
1.能夠在具備對象引用返回類型的方法中返回null。
2.數組是徹底合法的返回類型。
public String[] go(){ return new String[]{"Neil","Neo","Nail"}; }
3.在具備基本返回類型的方法內,能夠返回任何值或變量,只要它們可以隱式轉換爲所聲明的返回類型。
public int foo(){ char c ='c'; return c; }
4.在具備基本返回類型的方法內,能夠返回任何值或變量,只要它們可以顯式地強制轉換爲所聲明的返回類型。
public int foo(){ float f = 32.5f; return (int) f; }
5.必定不能從返回類型爲void的方法返回任何值。
6.在具備對象引用返回類型的方法內,能夠返回任何對象類型,只要它們可以隱式地強制轉換爲所聲明的返回類型。換句話說,能經過IS-A測試的(也就是使用instanceof運算符測試爲true)任何對象都可以從那個方法中返回。
//聲明返回超類,實際返回子類 public Animal getAnimal(){ return new Horse(); //Assume Horse extends Animal } //聲明返回超級父類Object,實際返回數組 public Object getObject(){ int[] nums = {1,2,3}; return nums; //Return an int array,which is still an object } //聲明返回接口,實際返回接口的一個實現類 public interface Chewable{} public class Gum implements Chewable{} public class TestChewable{ //Method with an interface return type public Chewable getChewable(){ return new Gum(); //Return interface implementer } }
2.8 構造函數和實例化
考試目標1.6 給定一組類和超類,爲一個或多個類編寫構造函數。給定一個類聲明,半段是否會建立默認構造函數。若是會,請肯定該構造函數的行爲。給定一個嵌套類或非嵌套類清單,編寫代碼,實例化該類。
考試目標5.3 解釋繼承對構造函數、實例或靜態變量,以及實例或靜態方法在修飾符方面的影響。
考試目標5.4 給定一個場景,編寫代碼,聲明和/或調用重寫方法或重載方法。編寫代碼,聲明和/或調用超類、重寫構造函數或重載構造函數。
構造函數基礎:
- 構造函數是用來建立新對象的,每當咱們「new」的時候,JVM就會按照你所指定的構造函數來建立一個對象實例。
- 每一個類都至少有一個構造函數。
- 構造函數都沒有返回類型(有就成方法了)。不一樣的構造函數經過不一樣的變元來區分(或者爲空)。
構造函數鏈:
當 Horse h = new Horse(); 的時候究竟發生了什麼?(Horse extends Animal,Animal extends Object)
- 調用Horse構造函數。經過一個對super()的(隱式)調用,每一個構造函數都會調用其超類的構造函數,除非構造函數調用同一個類的重載構造函數。
- 調用Animal構造函數(Animal是Horse的超類)。
- 調用Object構造函數(Object是全部類的最終超類,所以,Animal類擴展Object)。這時,咱們處於棧的頂部。
- 爲Object實例變量賦予顯式值。
- Object構造函數完成。
- 爲Animal實例變量賦予顯式值。
- Animal構造函數完成。
- 爲Horse實例變量賦予顯式值。
- Horse構造函數完成。
構造函數規則:
- 構造函數能使用任何訪問修飾符。
- 構造函數名稱必須與類名匹配。
- 構造函數必定不能有返回類型。
- 讓方法具備與類相同的名稱是合法的,可是建議不要這樣作。
- 若是不在類代碼中鍵入構造函數,編譯器將自動生成默認構造函數。
- 默認構造函數老是無變元構造函數。
- 若是在類代碼中已經有帶變元的構造函數存在,而沒有無變元的構造函數,那在編譯時不會自動生成無變元構造函數。
- 每一個構造函數都必須將對重載構造函數[this()]或超類構造函數[super()]的調用做爲第一條語句。若是沒有編譯器會自動插入super();
- 除非在超類構造函數運行以後,不然不能調用實例方法或訪問實例變量。
- 只能將靜態變量和方法做爲調用super()或this()的一部分進行訪問。例如:super(Animal.NAME)
- 抽象類具備構造函數,這些構造函數老是在實例化具體子類時才調用。
- 接口沒有構造函數。接口不是對象繼承樹的一部分。
- 調用構造函數的惟一方法是從另外一個構造函數內部進行調用。
2.8.1 判斷是否會建立默認構造函數
如何證實會建立默認構造函數?
只有在類代碼中沒有構造函數的,纔會生成默認構造函數。
如何知道它就是默認構造函數?
默認構造函數的特徵:
- 具備與類相同的訪問修飾符。
- 沒有任何變元。
- 包含super();
public class Foo{ public Foo(){ super(); } }
若是超類構造函數有變元會怎樣?
那在new的時候必須帶參 new Animal(「monkey」);
2.8.2 重載構造函數
重載構造的時候要注意:
- this()或super()必定要在第一行。
- 不要寫以下的死循環代碼:
class A { A(){ this("foo"); } A(String s){ this(); } }
2.9 靜態成員
考試目標1.3 編寫代碼,將基本類型、數組、枚舉和對象做爲靜態變量、實例變量和局部變量聲明、初始化並使用。此外,使用合法的標識符爲變量命名。
2.9.1 靜態變量和靜態方法
當方法永遠與實例徹底無關時,咱們就將它聲明爲static。
訪問靜態方法和變量:
- 用「 類名.靜態變量/方法 」來訪問。
- 靜態方法不能訪問實例(非靜態)變量。
- 靜態方法不能訪問非靜態方法。
- 靜態方法可以訪問靜態方法和靜態變量。
static方法的重定義問題:
咱們都知道靜態方法是不能被重寫的,可是能夠被重定義。這個問題很迷糊人,從代碼上來看,重寫和重定義沒有區別。那麼重定義(redefine)和重寫(override)有啥區別呢?
重定義操做的是靜態方法,靜態方法跟類有關;重寫操做的是非靜態方法,跟實例對象有關。看下下面的代碼:
public class Tenor extends Singer{ public static String sing(){ return "fa"; } public String sing2(){ return "fa2"; } public static void main(String[] args){ Tenor t = new Tenor(); Singer s = new Tenor(); System.out.println(t.sing()+" "+s.sing()+" "+t.sing2()+" "+s.sing2()); } } class Singer{ public static String sing(){ return "la"; } public String sing2(){ return "la2"; } } //運行結果是:fa la fa2 fa2
2.10 耦合與內聚
考試目標5.1 編寫代碼,實現類中的緊封裝、鬆耦合和高內聚,並描述這樣作的優勢。
書雲:「Sun考試對內聚和耦合所下的定義略帶主觀性」、「本章所討論的內容是從考試角度出發的,毫不是關於這兩條OO設計原則的真經」。
不少東西都是「兼容」標準,各自實現。
Java的OO設計目標:緊封裝、鬆耦合、高內聚。以實現易於建立、易於維護、易於加強的目標。
耦合(Coupling):耦合是指一個類瞭解另外一個類的程度。若是類A對類B的瞭解不多,僅限於類B統統過其接口公開的信息,類A並不知道B的更多具體實現,那就稱類A和類B是鬆耦合的。咱們說類B作到了緊封裝。
內聚(Cohesion):內聚用於表示一個類具備單一的、明確目標的程度。一個類的目標越明確,其內聚性越高。
書中舉了一個報表類的低內聚和高內聚例子,內聚性低的報表類將報表的保存、選擇庫、打印等方法都寫在一個類裏面。內聚性高的設計將這些目標明確,一個目標封裝在一個類裏面,如報表的打印類、選庫類、保存類等等。
我我的以爲凡事都講個度,要根據需求的複雜程度來決定設計,既不能讓運行的花銷太大,也不能爲了XXXX而死板的設計。