Java入門part6--繼承和多態

繼承


// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat {
	public String name;
	public Cat(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}
// Bird.java
class Bird {
	public String name;
	public Bird(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
	public void fly() {
		System.out.println(this.name + "is flying");
	}
}

觀察可見:

  • Animal,Cat,Bird三個類都有相同的方法eat(),且實現的功能一樣
  • 三個類都具有同樣的屬性name
  • 從邏輯上講,Cat,Bird都是Animal的一種(他們之間是is-a關係)

所以我們可以讓Cat,Bird繼承Animal類,實現代碼複用本質上來講繼承就是爲了代碼的複用

繼承的語法規則

extends

用關鍵字extends來實現繼承,

class 子類/派生類 extends 父類/基類/超類{

}

(像Cat,Bird這種類就叫做子類/派生類,而Animal這種被繼承的類叫做父類/基類/超類)

注意:

  • 使用 extends 指定父類.
  • Java 是單繼承,也就是說一個子類只能繼承一個父類 (而C++/Python等語言支持多繼承).
  • 子類會繼承父類的所有 public 的字段和方法.
  • 對於父類的 private 的字段和方法, 子類中是無法訪問的.
  • 子類的實例中, 也包含着父類的實例. 可以使用 super 關鍵字得到父類實例的引用.

super

super()//調用父類的構造方法 必須放在第一行   因爲要構造子類要先構造父類
super.func();//調用父類的方法func()
super.data;  //調用父類的數據成員data

父類只能訪問自己的成員 或者是方法
但子類可以通過關鍵字super訪問父類的成員和方法
所以我們可以將Animal,Cat,Bird的代碼優化如下:

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}
// Bird.java
class Bird extends Animal{
	public Bird(String name) {
		super(name);
	}
	public void fly() {
		System.out.println(this.name + "is flying");
	}
}

此時只需將Cat,Bird類中Animal有的字段和方法刪除即可
注意:
在子類的構造方法中一定要先用super()構造父類,構造完後再構造子類自己

但是此時如果想要實現封裝,將父類的name屬性變爲private,那麼編譯就會出錯,因爲子類無法訪問private修飾的方法和字段,此時應該如何實現封裝呢?

protected關鍵字

使用protected關鍵字就很好的解決了這個問題:

  • 對於類的調用者來說,並不能訪問protected修飾的字段和方法
  • 但對於子類來說,protected所修飾的字段和方法是可以訪問的

此處拓展幾個權限修飾關鍵字的修飾範圍:(default是什麼關鍵字都不加)

範圍 private default protected public
同一包中同一類
同一包中不同類 ×
不同包中的子類 × ×
不同包中非子類 × × ×

注意:
final所修飾的類不可被繼承

注意區分繼承和組合

  • 繼承是一種is-a關係
  • 組合是一種has-a關係,是一種包含關係

組合並沒有涉及到特殊的語法(諸如 extends 這樣的關鍵字), 僅僅是將一個類的實例作爲另外一個類的字段.比如:

class Student{
...
}
class Teacher{
...
}
class School{
	public Student[] students;
	public Teacher[] teacher;
	...
}

多態


向上轉型

即將子類的值賦值給父類 /父類引用子類對象,比如:

Cat cat = new Cat("大不妞");

可以寫成

Animal cat = new Cat("大不妞");

此時 cat 是父類 (Animal) 的引用, 指向子類 (Cat) 的實例. 這種寫法稱爲 向上轉型

向上轉型發生的時機 :

  • 直接賦值
  • 方法傳參
  • 方法返回

上面列舉的是直接賦值,方法傳參和方法返回見下:

方法傳參的形式()

public class Test {
	public static void main(String[] args) {
		Cat cat = new Cat("大不妞");
		feed(cat);
	}
	public static void feed(Animal animal) {
		animal.eat(" fish");
	}
}

方法返回

public class Test {
	public static void main(String[] args) {
		Animal animal = findMyAnimal();
	}
	public static Animal findMyAnimal() {
		Cat cat = new Cat("大不妞");
		return cat;
	}
}

動態綁定

將前面示例的代碼稍作改動,讓子類Cat和Animal有一個同名但實現功能不同的方法eat();

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println("我是一隻小動物");
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	public void eat(String food) {
		System.out.println("我是一隻小貓咪");
		System.out.println(this.name + "is eating" + food);
	}
}

class Demo0223 {
    public static void main(String[] args) {
        Animal animal1 = new Animal("大不妞");
        animal1.eat(" fish");

        Animal animal2 = new Cat("大不妞");
        animal2.eat(" fish");
    }
}

執行結果:
在這裏插入圖片描述
此時, 我們發現:

  • animal1 和 animal2 雖然都是 Animal 類型的引用, 但是 animal1 指向 Animal 類型的實例, 而animal2 指向的是Cat 類型的實例.
  • animal1 和 animal2 分別調用 eat 方法, 發現 animal1.eat() 實際調用了父類的方法, 而animal2.eat() 實際調用了子類的方法.

由此可得:
在 Java 中, 子類和父類擁有同名方法,此時調用該方法時究竟執行的是子類的方法還是父類的方法 , 要看究竟這個引用指向的是子類對象還是父類對象. 這個過程是程序運行時決定的(而不是編譯期), 因此稱爲 動態綁定

反彙編發現程序編譯時確實調用的是父類的方法 但是運行時卻調用的子類的方法 這就是運行時綁定(也叫動態綁定),這就是所謂的 編譯看左,運行看右

方法重寫(override)

像是上述代碼當中的eat():
子類實現父類的同名方法, 並且參數的類型和個數完全相同, 這種情況稱爲 方法覆寫/重寫/覆蓋(Override)

方法重寫的注意事項:

  1. 普通方法可以重寫, static 修飾的靜態方法不能重寫
  2. 重寫中子類的方法的訪問權限不能低於父類的方法訪問權限(也就是說如果父類方法是用protected修飾,那麼子類方法肯定不能是public修飾)

對於重寫的方法可以顯示的給一個註解@override

class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	@override
	public void eat(String food) {
		System.out.println("我是一隻小貓咪");
		System.out.println(this.name + "is eating" + food);
	}
}

這樣做的好處在於這個註解能幫我們進行一些合法性校驗.
例如不小心將方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫.

重寫和重載的區別:

方法重寫 方法重載
方法名 相同 相同
參數列表 相同 不同
返回值 相同 不做要求
範圍 繼承 同一個類
限制 被重寫的方法不能擁有比父類更嚴格的訪問控制權限 沒有訪問控制權限要求

發生多態要滿足兩個條件:(這個多態叫做運行時多態)

  1. 父類需要引用子類對象(即向上轉型)
  2. 通過父類的引用調用子類和父類同名的覆蓋方法

class對象存儲位置在方法區
反射: 獲取class對象(用三種方法會發現class對象地址一樣==》class對象只有一個)

向下轉型

向下轉型是將子類對象轉給父類,一般不太常見,下面將介紹他的作用

還是剛剛這段代碼

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println("我是一隻小動物");
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	@override
	public void eat(String food) {
		System.out.println("我是一隻小貓咪");
		System.out.println(this.name + "is eating" + food);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}

讓貓咪吃東西

        Animal animal = new Cat("大不妞");
        animal.eat(" fish");
//執行結果
//大不妞 is eating fish

如果我們想讓貓咪跑起來

animal.jump();

此時編譯出錯,找不到jump();方法
因爲編譯看左,運行看右,編譯時期編譯器先在Animal類中看有沒有jump方法,沒有所以直接編譯出現錯誤
那如果想要讓貓咪跑起來就只能

		Animal animal = new Cat("大不妞");
        Cat cat = (Cat)animal;
        animal.jump();

這種就是向下轉型,但是向下轉型存在風險,比如:

		Animal animal = new Bird("啾啾");
        Cat cat = (Cat)animal;
        animal.jump();
//此時執行會拋出類型轉換異常 java.lang.ClassCastException

因爲本質上animal是一個Bird類型的,和Cat直接沒有關係,所以就會出現類型轉換異常
==》要發生向下轉型最好先判斷是否是一個實例

instanseof

instanseof可以判定一個引用是否是某個類的實例

if(Animal instanseof Cat){
    Cat cat=(Cat)animal;
    cat.jump();
}

構造方法內是否可以發生運行時綁定?

答案是可以,例子見下

class A {
    public A() {
        func();
    }
	public void func() {
    	System.out.println("A.func()");
    }   
}

class B extends A {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("B.func() " + num);
    }
}
public class Test {
    public static void main(String[] args) {
        B b = new B(); 
    }
}
// 執行結果
B.func() 0

爲什麼執行出來的num會是0?
構造子類對象前要先構造父類
所以構造 B 對象的同時, 會調用 A 的構造方法.
A 的構造方法中調用了 func 方法, 此時會觸發動態綁定, 會調用到 B 中的 func
此時 B 對象自身還沒有構造, 此時 num 處在未初始化的狀態, 值爲 0.

使用多態的好處是什麼?

  1. 類調用者對類的使用成本進一步降低.
    封裝 是讓類的調用者不需要知道類的實現細節.
    多態 能讓類的調用者連這個類的類型是什麼都不必知道, 只需要知道這個對象具有某個方法即可.
    因此, 多態可以理解成是封裝的更進一步, 讓類調用者對類的使用成本進一步降低.

  2. 能夠降低代碼的 “圈複雜度”(一段代碼中的分支和循環語句越多,圈複雜度越高), 避免使用大量的 if - else

  3. 可擴展能力更強,使用多態的方式代碼改動成本也比較低.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章