天天看點

【Java】面向對象:繼承、組合和多态

一. 面向對象三大特性之繼承

1. 繼承的概念

繼承(inheritance)機制:是面向對象程式設計使代碼可以複用的最重要的手段,它允許程式員在保持原有類特性的基礎上進行擴充,增加新功能,這樣産生新的類,稱派生類(子類)。

繼承呈現了面向對象程式設計的層次結構, 展現了由簡單到複雜的認知過程。繼承主要解決的問題是:共性的抽取,實作代碼複用。

例如:狗和貓都是動物,那麼我們就可以将共性的内容進行抽取,然後采用繼承的思想來達到共用

【Java】面向對象:繼承、組合和多态

上述圖示中,Dog和Cat都繼承了Animal類,其中:Animal類稱為父類/基類/超類,Dog和Cat可以稱為Animal的子類/派生類,繼承之後,子類可以複用父類中成員,子類在實作時隻需關心自己新增加的成員即可。

2. 繼承的文法

在Java中如果要表示類之間的繼承關系,需要借助extends關鍵字,具體如下:

修飾符 class 子類 extends 父類 {
// ...
}      

此時将 1 中的設計思想使用代碼實作:

// Animal.java
public class Animal{
    String name;
    int age;
    public void eat(){
        System.out.println(name + "正在吃飯");
    }
    public void sleep(){
        System.out.println(name + "正在睡覺");
    }
}

// Dog.java
public class Dog extends Animal{
    void bark(){
        System.out.println(name + "汪汪汪~~~");
    }
}

// Cat.Java
public class Cat extends Animal{
    void mew(){
        System.out.println(name + "喵喵喵~~~");
    }
}

// TestExtend.java
public class TestExtend {
    public static void main(String[] args) {
        Dog dog = new Dog();
        // dog類中并沒有定義任何成員變量,
        //name和age屬性是從父類Animal中繼承下來的
        System.out.println(dog.name);
        System.out.println(dog.age);
        //dog通路的eat()和sleep()方法也是從Animal中繼承下來的
        dog.eat();
        dog.sleep();
        dog.bark();
    }
}      

注意:

1.子類會将父類中的成員變量或者成員方法繼承到子類中了

2.子類繼承父類之後,必須要新添加自己特有的成員,展現出與基類的不同,否則就沒有必要繼承了

3. 父類成員的通路

3.1 子類中通路父類的成員變量

1.子類和父類不存在同名成員變量

public class Base {
    int a;
    int b;
}

public class Derived extends Base{
    int c;
    public void method(){
        a = 10; // 通路從父類中繼承下來的a
        b = 20; // 通路從父類中繼承下來的b
        c = 30; // 通路子類自己的c
    }
}      

2.子類和父類成員變量同名

public class Base {
    int a;
    int b;
    int c;
}

public class Derived extends Base{
    int a; // 與父類中成員a同名,且類型相同
    char b; // 與父類中成員b同名,但類型不同
    public void method(){
        a = 100; // 通路子類自己新增的a
        b = 101; // 通路子類自己新增的b
        c = 102; // 子類沒有c,通路從父類繼承下來的c
        // d = 103; // 編譯失敗,因為父類和子類都沒有定義成員變量d
    }
}      

在子類方法中 或者 通過子類對象通路成員時:

• 如果通路的成員變量子類中有,優先通路自己的成員變量。

• 如果通路的成員變量子類中無,則通路父類繼承下來的,如果父類也沒有定義,則編譯報錯。

• 如果通路的成員變量與父類中成員變量同名,則優先通路自己的。

成員變量通路遵循就近原則,自己有優先自己的,如果沒有則向父類中找

3.2 子類中通路父類的成員方法

1.父類和子類的成員方法名字不同

public class Base {
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
}

public class Derived extends Base{
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
    public void methodC(){
        methodB(); // 通路子類自己的methodB()
        methodA(); // 通路父類繼承的methodA()
        // methodD(); // 編譯失敗,在整個繼承體系中沒有發現方法methodD()
    }
}      

2.父類和子類的成員方法名字相同

public class Base {
    int a;
    int b;
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
    public void methodB(){
        System.out.println("Base中的methodB()");
    }
}

public class Derived extends Base{
    public void methodA(int a) {
        System.out.println("Derived中的method(int)方法");
    }
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
    public void methodC(){
        methodA(); // 沒有傳參,通路父類中的methodA()
        methodA(20); // 傳遞int參數,通路子類中的methodA(int)
        methodB(); // 直接通路,則永遠通路到的都是子類中的methodB(),基類的無法通路到
    }
}      

【總結】:

• 通過子類對象通路父類與子類中不同名方法時,優先在子類中找,找到則通路;否則在父類中找,找到則通路,如果父類中也沒有則編譯報錯。

• 通過派生類對象通路父類與子類同名方法時,如果父類和子類同名方法的參數清單不同(重載),根據調用方法适傳遞的參數選擇合适的方法通路;如果沒有則報錯。

• 通過派生類對象通路父類與子類同名方法時,如果父類和子類同名方法的參數清單、傳回值都相同,則遵循就近原則,直接通路子類中的方法,不會通路父類中的方法。

4. protected 的使用場景

【Java】面向對象:繼承、組合和多态

結合下面代碼了解父類中不同通路權限的成員在子類中的可見性:

注意:如果和子類在不同包中,父類中的成員被protected修飾,要想通路父類中的成員必須通過super關鍵字來通路;父類中private成員變量雖然在子類中不能直接通路,但是也繼承到子類中了

// extend01包中
public class B {
    private int a;
    protected int b;
    public int c;
    int d;
} 

// extend01包中
// 同一個包中的子類
public class D extends B{
    public void method(){
     // super.a = 10; // 編譯報錯,父類private成員在相同包子類中不可見
        super.b = 20; // 父類中protected成員在相同包子類中可以直接通路
        super.c = 30; // 父類中public成員在相同包子類中可以直接通路
        super.d = 40; // 父類中預設通路權限修飾的成員在相同包子類中可以直接通路
    }
} 

// extend02包中
// 不同包中的子類
public class C extends B {
    public void method(){
      //super.a = 10; // 編譯報錯,父類中private成員在不同包子類中不可見
        super.b = 20; // 父類中protected修飾的成員在不同包子類中可以直接通路
        super.c = 30; // 父類中public修飾的成員在不同包子類中可以直接通路
      //super.d = 40; // 父類中預設通路權限修飾的成員在不同包子類中不能直接通路
    }
} 

// extend02包中
// 不同包中的類
public class TestC {
    public static void main(String[] args) {
        C c = new C();
        c.method();
// System.out.println(c.a); // 編譯報錯,父類中private成員在不同包其他類中不可見
// System.out.println(c.b); // 父類中protected成員在不同包其他類中不能直接通路
        System.out.println(c.c); // 父類中public成員在不同包其他類中可以直接通路
// System.out.println(c.d); // 父類中預設通路權限修飾的成員在不同包其他類中不能直接通路
    }
}      

5. 繼承方式

但在Java中隻支援以下幾種繼承方式:

【Java】面向對象:繼承、組合和多态

注意:Java中不支援多繼承,且一般我們不希望出現超過三層的繼承關系。

6. final 關鍵字

final關鍵可以用來修飾變量、成員方法以及類。

1.修飾變量或字段,表示常量(即不能修改)

final int a = 10;
a = 20; // 編譯出錯,這裡a是常量,不可以被修改      

2.修飾類:表示此類不能被繼承

final public class Animal {
...
}

public class Bird extends Animal {
...
} 

// 編譯出錯
Error:(3, 27) java: 無法從最終com.bit.Animal進行繼      

觀察 String 字元串類的源碼, 預設就是用 final 修飾的, 不能被繼承.

【Java】面向對象:繼承、組合和多态

3.修飾方法:final修飾的方法叫做密封方法,不能被重寫。

7. 繼承與組合

和繼承類似, 組合也是一種表達類之間關系的方式, 也是能夠達到代碼重用的效果。組合并沒有涉及到特殊的文法(諸如 extends 這樣的關鍵字), 僅僅是将一個類的執行個體作為另外一個類的字段。

繼承表示對象之間是is-a的關系,比如:狗是動物,貓是動物

組合表示對象之間是has-a的關系,比如:汽車和其輪胎、發動機、方向盤、車載系統等的關系就應該是組合,因為汽車是有這些部件組成的。

// 輪胎類
class Tire{
// ...
} /
        / 發動機類
class Engine{
// ...
} /
        / 車載系統類
class VehicleSystem{
// ...
}
class Car{
    private Tire tire; // 可以複用輪胎中的屬性和方法
    private Engine engine; // 可以複用發動機中的屬性和方法
    private VehicleSystem vs; // 可以複用車載系統中的屬性和方法
// ...
} /
        / 奔馳是汽車
class Benz extend Car{
// 将汽車中包含的:輪胎、發送機、車載系統全部繼承下來
}      

組合和繼承都可以實作代碼複用,應該使用繼承還是組合,需要根據應用場景來選擇,一般建議:能用組合盡量用組合。

二. 面向對象三大特性之多态

1. 多态的概念

俗來說,就是多種形态,具體點就是去完成某個行為,當不同的對象去完成時會産生出不同 的狀态;同一件事情,發生在不同對象身上,就會産生不同的結果。

【Java】面向對象:繼承、組合和多态

2. 重寫

重寫(override):也稱為覆寫。重寫是子類對父類非靜态、非private修飾,非final修飾,非構造方法等的實作過程進行重新編寫, 傳回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據需要,定義特定于自己的行為。 也就是說子類能夠根據需要實作父類的方法。

【方法重寫的規則】

• 子類在重寫父類的方法時,一般必須與父類方法原型一緻: 傳回值類型 方法名 (參數清單) 要完全一緻

• 被重寫的方法傳回值類型可以不同,但是必須是具有父子關系的

• 通路權限不能比父類中被重寫的方法的通路權限更低。例如:如果父類方法被public修飾,則子類中重寫該方法就不能聲明為 protected

• 父類被static、private、final修飾的方法,構造方法都不能被重寫。

• 重寫的方法, 可以使用 @Override 注解來顯式指定. 有了這個注解能幫我們進行一些合法性校驗. 例如不小心将方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫

【重寫和重載的差別】

方法重載是一個類的多态性表現,而方法重寫是子類與父類的一種多态性表現

【Java】面向對象:繼承、組合和多态
【Java】面向對象:繼承、組合和多态

【重寫的設計原則】

對于已經投入使用的類,盡量不要進行修改。最好的方式是:重新定義一個新的類,來重複利用其中共性的内容,并且添加或者改動新的内容。

靜态綁定:也稱為前期綁定(早綁定),即在編譯時,根據使用者所傳遞實參類型就确定了具體調用那個方法。典型代表函數重載。

動态綁定:也稱為後期綁定(晚綁定),即在編譯時,不能确定方法的行為,需要等到程式運作時,才能夠确定具體調用那個類的方法。 這也是多态的特征。

3. 向上轉型和向下轉型

3.1 向上轉型

向上轉型:實際就是建立一個子類對象,将其當成父類對象來使用

文法格式:父類類型 對象名 = new 子類類型( )

//animal是父類類型,但可以引用一個子類對象,因為是從小範圍向大範圍的轉換。
Animal animal = new Cat("元寶",2);      

【使用場景】

1.直接指派

2.方法傳參

3.方法傳回

public class TestAnimal {
// 2. 方法傳參:形參為父類型引用,可以接收任意子類的對象
    public static void eatFood(Animal a){
        a.eat();
    }

    // 3. 作傳回值:傳回任意子類對象
    public static Animal buyAnimal(String var){
        if("狗" == var){
            return new Dog("狗狗",1);
        }else if("貓" == var){
            return new Cat("貓貓", 1);
        }else{
            return null;
        }
    }
    public static void main(String[] args) {
        Animal cat = new Cat("元寶",2);
    // 1. 直接指派:子類對象指派給父類對象
        Dog dog = new Dog("小七", 1);
        eatFood(cat);
        eatFood(dog);
        Animal animal = buyAnimal("狗");
        animal.eat();
        animal = buyAnimal("貓");
        animal.eat();
    }
}

public class Animal{
    String name;
    int age;
    public void eat(){
        System.out.println(name + "正在吃飯");
    }
    public void sleep(){
        System.out.println(name + "正在睡覺");
    }
}

// Dog.java
public class Dog extends Animal{
    void bark(){
        System.out.println(name + "汪汪汪~~~");
    }
}

// Cat.Java
public class Cat extends Animal{
    void mew(){
        System.out.println(name + "喵喵喵~~~");
    }
}      

向上轉型的優點:讓代碼實作更簡單靈活。

向上轉型的缺陷:不能調用到子類特有的方法。

3.2 向下轉型

将一個子類對象經過向上轉型之後當成父類方法使用,再無法調用子類的方法,但有時候可能需要調用子類特有的方法,此時:将父類引用再還原為子類對象即可,即向下轉型

public class TestAnimal {
    public static void main(String[] args) {
        Cat cat = new Cat("元寶",2);
        Dog dog = new Dog("小七", 1);
        
        // 向上轉型
        Animal animal = cat;
        animal.eat();
        animal = dog;
        animal.eat();
        
        // 編譯失敗,編譯時編譯器将animal當成Animal對象處理
        // 而Animal類中沒有bark方法,是以編譯失敗
        // animal.bark();
        
        // 向上轉型
        // 程式可以通過程式設計,但運作時抛出異常---因為:animal實際指向的是狗
        // 現在要強制還原為貓,無法正常還原,運作時抛出:ClstException
        cat = (Cat)animal;
        cat.mew();
        
        // animal本來指向的就是狗,是以将animal還原為狗也是安全的
        dog = (Dog)animal;
        dog.bark();
    }
}      

向下轉型用的比較少,而且不安全,萬一轉換失敗,運作時就會抛異常。Java中為了提高向下轉型的安全性,引入了 instanceof ,如果該表達式為true,則可以安全轉換。

instanceof 是 Java 的保留關鍵字。它的作用是測試它左邊的對象是否是它右邊的類的執行個體,傳回 boolean 的資料類型。

public class TestAnimal {
    public static void main(String[] args) {
        Cat cat = new Cat("元寶",2);
        Dog dog = new Dog("小七", 1);
        
        // 向上轉型
        Animal animal = cat;
        animal.eat();
        animal = dog;
        animal.eat(); 
        
        if(animal instanceof Cat){
            cat = (Cat)animal;
            cat.mew();
        } 
        if(animal instanceof Dog){
            dog = (Dog)animal;
            dog.bark();
        }
    }
}      

4. 多态實作條件

在java中要實作多态,必須要滿足如下幾個條件,缺一不可:

1.必須在繼承體系下

2.子類必須要對父類中方法進行重寫

3.通過父類的引用調用重寫的方法

多态展現:在代碼運作時,當傳遞不同類對象時,會調用對應類中的方法。

實作多态的執行個體:

public class Animal {
    String name;
    int age;
    public Animal(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void eat(){
        System.out.println(name + "吃飯");
    }
}
public class Cat extends Animal{
    public Cat(String name, int age){
        super(name, age);
    } 
    @Override
    public void eat(){
        System.out.println(name+"吃魚~~~");
    }
}
public class Dog extends Animal {
    public Dog(String name, int age){
        super(name, age);
    } 
    @Override
    public void eat(){
        System.out.println(name+"吃骨頭~~~");
    }
}

///分割線//

public class TestAnimal {
// 編譯器在編譯代碼時,并不知道要調用Dog 還是 Cat 中eat的方法
// 等程式運作起來後,形參a引用的具體對象确定後,才知道調用那個方法
// 注意:此處的形參類型必須時父類類型才可以
    public static void eat(Animal a){
        a.eat();
    }
    public static void main(String[] args) {
        Cat cat = new Cat("元寶",2);
        Dog dog = new Dog("小七", 1);
        eat(cat);
        eat(dog);
    }
}      

執行結果:

【Java】面向對象:繼承、組合和多态

在上述代碼中, 分割線上方的代碼是 類的實作者 編寫的, 分割線下方的代碼是 類的調用者(調用類中的方法) 編寫的。

當類的調用者在編寫 eat 這個方法的時候, 參數類型為 Animal (父類), 此時在該方法内部并不知道, 也不關注目前的 a 引用指向的是哪個類型(哪個子類)的執行個體. 此時 a這個引用調用 eat方法可能會有多種不同的表現(和 a 引用的執行個體相關), 這種行為就稱為 多态.

【Java】面向對象:繼承、組合和多态

5. 多态的優缺點

5.1 使用多态好處

1.能夠降低代碼的 “圈複雜度”, 避免使用大量的 if - else

圈複雜度是一種描述一段代碼複雜程度的方式,計算一段代碼中條件語句和循環語句出現的個數, 這個個數就稱為 “圈複雜度”;如果一個方法的圈複雜度太高, 就需要考慮重構,不同公司對于代碼的圈複雜度的規範不一樣. 一般不會超過 10 。

例如要列印多個形狀, 如果不基于多态, 實作代碼如下:

public class Test{
    public static void drawShapes() {
        Rect rect = new Rect();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
        for (String shape : shapes) {
            if (shape.equals("cycle")) {
                cycle.draw();
            } else if (shape.equals("rect")) {
                rect.draw();
            } else if (shape.equals("flower")) {
                flower.draw();
            }
        }
    }

    public static void main(String[] args) {
        drawShapes();
    }
}

class Shape {
    //屬性....
    public void draw() {
        System.out.println("畫圖形!");
    }
}
class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("♦");
    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("●");
    }
}

class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}      

如果使用使用多态, 則不必寫這麼多的 if - else 分支語句, 代碼更簡單

public static void drawShapes() {
    // 我們建立了一個 Shape 對象的數組.
        Shape[] shapes = {
                new Cycle(),
                new Rect(),
                new Cycle(),
                new Rect(),
                new Flower()
        };

        for (Shape shape : shapes) {
            shape.draw();
        }
}      
class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}      

5.2 多态的缺陷