Java物件導向04——三大特性之多型

白夜的白發表於2024-05-08

多型

1、什麼是多型

在Java中,多型是物件導向程式設計中的一個重要概念,它允許不同型別的物件對同一方法進行不同的實現。具體來說,多型性指的是透過父類的引用變數來引用子類的物件,從而實現對不同物件的統一操作

05

2、多型實現的條件

在Java中,要實現多型性,就必須滿足以下條件:

  1. 繼承關係

    存在繼承關係的類之間才能夠使用多型性。多型性通常透過一個父類用變數引用子類物件來實現。

  2. 方法重寫

    子類必須重寫(Override)父類的方法。透過在子類中重新定義和實現父類的方法,可以根據子類的特點行為改變這個方法的行為,如貓和狗吃東西的獨特行為。

  3. 父類引用指向子類物件

    使用父類的引用變數來引用子類物件。這樣可以實現對不同型別的物件的統一操作,而具體呼叫哪個子類的方法會在執行時多型決定

例如,下面的案例是根據貓和狗叫的動作的不同,而實現的多型:

class Animal {
    public void sound() {
        System.out.println("動物發出聲音");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("狗發出汪汪聲");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("貓發出喵喵聲");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog(); // 父類引用指向子類物件
        Animal animal2 = new Cat(); // 父類引用指向子類物件

        animal1.sound(); // 輸出:狗發出汪汪聲
        animal2.sound(); // 輸出:貓發出喵喵聲
    }
}

在這個示例中,Animal 類是父類,DogCat 類是它的子類。透過將父類的引用變數分別指向子類物件,實現了多型性。在執行時,根據引用變數的實際型別來呼叫相應的子類方法,從而輸出不同的聲音。

3、重寫(Overrride)

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

方法重寫的規則:

  • 子類在重寫父類的方法時,一般必須與父類方法原型一致: 返回值型別 方法名 (引數列表) 要完全一致
  • 被重寫的方法返回值型別可以不同,但是必須是具有父子關係的
  • 訪問許可權不能比父類中被重寫的方法的訪問許可權更低。例如:如果父類方法被 public 修飾,則子類中重寫該方法就不能宣告為 protected
  • 重寫的方法, 可以使用 @Override 註解來顯式指定. 有了這個註解能幫我們進行一些合法性校驗. 例如不小心將方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫
  • 重寫的方法不能丟擲比父類中被重寫的方法更多或更寬泛的異常。子類中重寫的方法可以丟擲相同的異常或更具體的異常,或者不丟擲異常。
    • 例如,如果父類的方法宣告丟擲 IOException,則子類中重寫的方法可以丟擲 IOExceptionFileNotFoundException,或者不丟擲異常,但不能丟擲比 IOException 更通用的異常,如 Exception
  • 重寫的方法必須具有相同的方法體,或者可以進行方法體的擴充套件。
    • 子類中重寫的方法可以呼叫父類中被重寫的方法,使用 super 關鍵字。

看下面的示例:

class Mineral {
    public void mine() {
        System.out.println("礦物");
    }
}
class Iron extends Mineral {
    @Override
    public void mine() {
        System.out.println("鐵礦");
    }
    public void broked() {
        System.out.println("石鎬可以破壞它");
    }
}
class Diamond extends Mineral {
    @Override
    public void mine() {
        System.out.println("鑽石");
    }
    public void what() {
        System.out.println("掉落鑽石");
    }
}

呼叫:

public class Minecraft {
    public static void main(String[] args) {
        Mineral p = new Iron();
        p.mine();
        // 呼叫特有的方法
        Iron s = (Iron) p;
        s.broked();
        // ((Iron) p).broked();
    }
}

輸出:

鐵礦
石鎬可以破壞它

**重寫和過載的區別: **

  1. 定義位置:過載方法定義在同一個類中,而重寫方法定義在父類和子類之間。
  2. 方法簽名:過載方法具有相同的名稱,但方法簽名(引數型別和個數)不同。重寫方法具有相同的名稱和方法簽名。
  3. 繼承關係:過載方法不涉及繼承關係,可以在同一個類中定義。重寫方法是在子類中對父類方法的重新定義和實現。
  4. 執行時呼叫:過載方法是根據方法的引數列表的不同進行靜態繫結,在編譯時確定。重寫方法是根據物件的實際型別進行動態繫結,在執行時確定。
  5. 目的:過載方法用於在同一個類中實現相似功能但具有不同引數的方法。重寫方法用於子類重新定義父類方法的行為,以適應子類的特定需求。

總結來說,過載是在同一個類中根據引數列表的不同定義多個具有相同名稱但引數不同的方法,而重寫是子類重新定義和實現了從父類繼承的方法。過載方法透過靜態繫結在編譯時確定呼叫,重寫方法透過動態繫結在執行時確定呼叫。過載用於實現相似功能但具有不同引數的方法,重寫用於改變父類方法的行為以適應子類的需求

圖片06

即:方法過載是一個類的多型性表現,而方法重寫是子類與父類的一種多型性表現。

重寫的設計原則:
對於已經投入使用的類,儘量不要進行修改。最好的方式是:重新定義一個新的類,來重複利用其中共性的內容,並且新增或者改動新的內容。

靜態繫結:也稱為前期繫結(早繫結),即在編譯時,根據使用者所傳遞實參型別就確定了具體呼叫那個方法。典型代表函式過載。

動態繫結:也稱為後期繫結(晚繫結),即在編譯時,不能確定方法的行為,需要等到程式執行時,才能夠確定具體呼叫那個類的方法。

多型實現的就是動態繫結。

4、向上轉型和向下轉型

向上轉型:

​ 向上轉型(Upcasting)是指將一個子類的物件引用賦值給其父類型別的引用變數。這是在物件導向程式設計中的一種常見操作,用於實現多型性和靈活的物件處理。

​ 在向上轉型中,子類物件可以被視為父類物件,可以使用父類型別的引用變數來引用子類物件。這樣做的好處是可以以統一的方式處理不同型別的物件,實現程式碼的靈活性和可擴充套件性。

向上轉型的特點和規則如下:

  1. 子類物件可以隱式地轉型為父類物件,不需要任何顯式的型別轉換操作。
  2. 父類引用變數可以引用子類物件,但透過父類引用變數只能訪問到子類物件中定義的父類成員,無法訪問子類獨有的成員。
  3. 子類物件中重寫的方法,在透過父類引用變數呼叫時,會呼叫子類中的實現(動態繫結)。
  4. 向上轉型是安全的操作,因為子類物件本身就是一個父類物件。

使用場景:

  1. 直接賦值
  2. 方法傳參
  3. 方法返回

下面看程式碼

public class TestAnimal {
// 2. 方法傳參:形參為父型別引用,可以接收任意子類的物件
	public static void eatFood(Animal a){
		a.eat();
	}
// 3. 作返回值:返回任意子類物件
	public static Animal buyAnimal(String var){
		if("狗".equals(var) ){
			return new Dog("狗狗",1);
		}else if("貓" .equals(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();
	}
}

在上述示例中,存在一個繼承關係:類 Dog 繼承自類 Animal。在 Main 類的 main 方法中,首先建立了一個 Dog 類的物件,並將其賦值給一個 Animal 型別的引用變數 animal,這就是向上轉型的過程。透過 animal 引用變數,可以呼叫 eat() 方法,而在執行時,實際執行的是 Dog 類中重寫的 eat() 方法。

需要注意的是,雖然 animal 引用變數的型別是 Animal,但是它指向的是一個 Dog 類的物件,因此可以將其重新轉型為 Dog 型別(向下轉型),並透過 dog 引用變數訪問 Dog 類中獨有的成員方法 bark()

總結起來,向上轉型允許將子類物件視為父類物件,以父類型別的引用變數來引用子類物件,實現多型性和靈活的物件處理。

向下轉型:

​ 將一個子類物件經過向上轉型之後當成父類方法使用,再無法呼叫子類的方法,但有時候可能需要呼叫子類特有的方法,此時:將父類引用再還原為子類物件即可,即向下轉換。

圖片07

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實際指向的是狗
	// 現在要強制還原為貓,無法正常還原,執行時丟擲:ClassCastException
		cat = (Cat)animal;
		cat.mew();
	// animal本來指向的就是狗,因此將animal還原為狗也是安全的
		dog = (Dog)animal;
		dog.bark();
	}
}

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

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();
		}
	}
}

5、多型的優缺點

假如有如下程式碼

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("❀");
	}
}

Java多型性的優點:

  • 程式碼的可複用性

    能夠降低程式碼的 “圈複雜度”, 避免使用大量的 if - else 上述程式碼如果不使用多型,就要使用大量的if - else語句,可以促進程式碼的複用

  • 可擴充套件能力更強:

    如果要新增一種新的形狀, 使用多型的方式程式碼改動成本也比較低.對於類的呼叫者來說(drawShapes方法), 只要建立一個新類的例項就可以了, 改動成本很低.而對於不用多型的情況, 就要把 drawShapes 中的 if - else 進行一定的修改, 改動成本更高

Java多型性的缺點:

  • 執行時效能損失:多型性需要在執行時進行方法的動態繫結,這會帶來一定的效能損失。相比於直接呼叫具體的子類方法,多型性需要在執行時確定要呼叫的方法,導致額外的開銷。
  • 程式碼可讀性下降:多型性使得程式碼的行為變得更加動態和不確定。在某些情況下,可能需要跟蹤程式碼中使用的物件型別和具體的方法實現,這可能降低程式碼的可讀性和理解性。
  • 限制訪問子類特有成員:透過父類型別的引用變數,只能訪問父類及其繼承的成員,無法直接訪問子類特有的成員。如果需要訪問子類特有的成員,就需要進行向下轉型操作,這增加了程式碼的複雜性和維護的難度。
class B {
	public B() {
		// do nothing
		func();
	}
	public void func() {
		System.out.println("B.func()");
	}
}
class D extends B {
	private int num = 1;
	@Override
	public void func() {
		System.out.println("D.func() " + num);
	}
}
public class Test {
	public static void main(String[] args) {
		D d = new D();
	}
}
//執行結果:
D.func() 0
  • 構造 D 物件的同時, 會呼叫 B 的構造方法.
  • B 的構造方法中呼叫了 func 方法, 此時會觸發動態繫結, 會呼叫到 D 中的 func
  • 此時 D 物件自身還沒有構造, 此時 num 處在未初始化的狀態, 值為 0. 如果具備多型性,num的值應該是1.
  • 所以在建構函式內,儘量避免使用例項方法,除了final和private方法。

總結:“用盡量簡單的方式使物件進入可工作狀態”,儘量不要在構造器中呼叫方法(如果這個方法被子類重寫,就會觸發動態繫結,但是此時子類物件還沒構造完成),可能會出現一些隱藏的但是又極難發現的問題。

相關文章