深度剖析—繼承和多型
- 繼承
- 什么是繼承?
- 繼承的語法
- 注意事項
- 繼承的意義
- super 關鍵字
- super 和 this 關鍵字的區別🔺
- protected 關鍵字
- 多層繼承
- final 關鍵字
- 繼承和組合
- 多型
- 多型概念
- 多型前提:
- 向上轉型
- 向下轉型
- 動態系結 (運行時系結)
- 重寫 override
- 重寫注意事項🔺
- 坑來了~
- 多型好處:
- 多型總結:
之前提到OOP語言的基本三大特性:繼承、封裝、多型
本篇重點討論:繼承和多型
繼承
什么是繼承?
先舉一個例子:
class Animal{
public String name;
public void eat(){
System.out.println("Animal::eat()");
}
public void sleep(){
System.out.println("Animal::sleep()");
}
}
class Cat{
public String name;
public void eat(){
System.out.println("Cat::eat()");
}
public void sleep(){
System.out.println("Cat::sleep()");
}
public void mem(){
System.out.println("Cat::mem()");
}
}
class Bird{
public String name;
public void eat(){
System.out.println("Bird::eat()");
}
public void fly(){
System.out.println("Bird::fly()");
}
}

由以上三個類,不難發現:
- 三個類都具備一個相同的 name 屬性,而且意義也完全一樣
- 三個類都具備一個相同的 eat 方法,而且行為也完全一樣
- 從邏輯上講,Cat 和 Bird 都是一種Animal
此時我們就可以讓 Cat 和 Bird 分別繼承 Animal類,來實作代碼復用的效果
Animal,這種被繼承的類,稱為:父類 / 基類 / 超類
Cat / Bird,這種繼承的類,稱為:子類 / 派生類
子類和父類的關系,就像現實生活中兒子繼承父親的財產類似,子類也會繼承父類的欄位和方法,以達到代碼復用的效果
繼承的語法
class 子類 extends 父類 {
…
}

注意事項
- 使用 extends 指定父類,Java中使用 extends 只能繼承一個類
- Java中的一個子類只能繼承一個父類,即:在Java中只有單繼承
(C++ / Python等語言支持多繼承) - 子類會繼承父類所有 public 的欄位和方法
- 對于父類中 private 的欄位和方法,子類中是無法訪問的
- 子類的實體中,也包含著父類的實體,可以使用 super 關鍵字得到父類實體的參考
則上述舉例改為繼承為:
class Animal{
public String name;
public void eat(){
System.out.println(this.name + "Animal::eat()");
}
public void sleep(){
System.out.println(this.name + "Animal::sleep()");
}
}
class Cat extends Animal{
public void mem(){
System.out.println("Cat::mem()");
}
}
class Bird extends Animal{
public void fly(){
System.out.println("Bird::fly()");
}
}
繼承的意義
子類繼承了父類除構造方法外所有的
面相物件思想中提出了繼承的概念,專門用來進行共性抽取,實作代碼復用,若不繼承的話,那么重復的代碼會非常多!
super 關鍵字
在子類方法中訪問父類的成員,
子類在構造的時候,要先幫助父類進行構造
class Animal{
public String name;
public Animal(String name){
this.name = name;
}
}
class Cat extends Animal{
public Cat(String name){
super(name); //顯式呼叫,不是繼承
System.out.println("Cat(String)");
}
}
super 和 this 關鍵字的區別🔺
①.this關鍵字:當前物件的參考
- this( );呼叫本類中其他的構造方法
- this.data;訪問當前類中的屬性
- this.func( );呼叫本類中其他的成員方法
②.super關鍵字:代表父類物件的參考
- super( );呼叫父類中的構造方法,必須放到第一行
- super.data;訪問父類中的屬性
- super.func( );訪問父類中的成員方法
protected 關鍵字
在類和物件篇,為了實作封裝特性,Java中引入了訪問限定符
主要限定:類或者類中成員能否在類外或者其他包中被訪問
| 訪問修飾符 | 本類 | 同包 | 子類 | 其他 |
|---|---|---|---|---|
| private | √ | |||
| public | √ | √ | √ | √ |
| protected | √ | √ | √ | |
| 默認(default) | √ | √ |
總結: private < default < protected < public
- private:只有類的內部能訪問
- public:類內部和類的呼叫者都能訪問
- protected:類內部 / 子類和同一個包中的類可以訪問,其他類不能訪問
- 默認(包訪問權限):類內部能訪問,同包中的類可以訪問,其他類不能訪問
若把欄位設定為 private 時,會發現子類不能訪問,但設成 public,又違背了"封裝",這就引入了 protected 關鍵字 ,其主要體現在繼承上
- 對于類的呼叫者來說,protected 修飾的欄位和方法是不能訪問的
- 對于類的子類和同一個包的其他類來說,protected修飾的欄位和方法是可以訪問的
1.同包中的同一類:
public class Animal {
protected String name;
}
2.同包中的不同類:
3.不同包中的子類:
多層繼承
class Animal{
protected String name;
public Animal(String name){
this.name = name;
System.out.println("Animal(String)");
}
public void eat(){
System.out.println(this.name + "Animal::eat()");
}
private void sleep(){
System.out.println(this.name + "Animal::sleep()");
}
}
class Cat extends Animal{
public Cat(String name){
super(name); //顯式呼叫,不是繼承
System.out.println("Cat(String)");
}
public void mem(){
System.out.println(this.name + "Cat::mem()");
}
}
class ChineseGardenCat extends Cat{
public ChineseGardenCat(String name){
super(name);
}
}

一般繼承關系不超過三層,若繼承層次太多,就需要考慮對代碼進行重構
final 關鍵字
如果想從語法上進行限制繼承,則可以使用 final 關鍵字
final關鍵可以用來修飾變數、成員方法以及類
1.修飾變數或欄位,表示常量
常量:即只能被初始化一次,故不能修改
final int a = 6;
a = 8; //編譯出錯
2.修飾類,表示此類不能被繼承
功能是 限制 類被繼承
final修飾類,也叫做:密封類
final public class Animal {
...
}
public class Bird extends Animal {
...
}
上述代碼會編譯出錯,final 修飾的類被繼承的時候,就會編譯報錯
我們平時是用的 String 字串類,就是 final 修飾的,不能被繼承
3.修飾方法,表示此類不能被繼承
繼承和組合
和繼承類似,組合也是一種表達類之間關系的方式,也是能夠達到代碼重用的效果
public class Students{
...
}
public class Teachers{
...
}
public class School{
public Students[] students;
public Teachers[] teachers
}
組合并沒有涉及到特殊的語法,僅僅是將一個類的實體作為另外一個類的欄位
繼承表示物件之間是 is-a 的關系
組合表示物件之間是 has-a 的關系
順序表,鏈表,都運用到了組合,可以翻看之前相關博客內容
組合和繼承都可以實作代碼復用,應該使用繼承還是組合,需要根據應用場景來選擇,一般建議:能用組合盡量用組合
多型
多型概念
同一操作作用于不同的物件,可以有不同的解釋,產生不同的執行結果,這就是多型性
簡單的說:就是用基類的參考指向子類的物件
多型前提:
- 父類參考子類物件
- 父類和子類有同名的覆寫方法
- 通過父類參考,呼叫這個重寫的方法
向上轉型
理解多型之前,需要理解:向上轉型—將子類物件賦值給父類參考


向上轉型后,通過父類的參考 只能訪問父類自己的方法和屬性,即:父類參考只能訪問自己特有的
向上轉型發生的幾種情況?
- 直接賦值
上邊例子就是直接賦值
Animal animal = new Cat("mimi");
- 傳參
public static void func(Animal animal){
animal.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("mimi");
func(cat);
}
- 回傳值
public static Animal func(){
Cat cat = new Cat("mimi");
return cat;
}
//回傳值 向上轉型
public static void main(String[] args) {
Animal animal = func();
animal.eat();
}
向下轉型
向上轉型是子類物件轉成父類物件,向下轉型是父類物件轉成子類物件,相比于向上轉型來說,向下轉型并不常見,但也有一定用途
舉例:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
System.out.println("Animal(String)");
}
public void eat(){
System.out.println(this.name + " " + "Animal::eat()");
}
private void sleep(){
System.out.println(this.name + " " + "Animal::sleep()");
}
}
class Bird extends Animal {
public Bird(String name){
super(name);
}
public void fly(){
System.out.println(this.name + " " + "Bird::fly()");
}
}
public static void main(String[] args) {
Animal animal = new Bird("卟卟");
animal.eat();
//向下轉型 父類的參考賦值給了子類
Bird bird = (Bird)animal;
bird.fly();
}
輸出結果:

向下轉型的不安全性:

為了提高向下轉型的安全性,引入了 instanceof,若該運算式為true,則可以安全轉換
public static void main(String[] args) {
Animal animal = new Cat("卟卟");
// A instanceof B 判斷A是否為B的實體
if(animal instanceof Bird){
Bird bird = (Bird)animal;
bird.fly();
}
else{
System.out.println("例外!");
}
}
輸出結果:例外!
向下轉型非常不安全,一般很少使用
動態系結 (運行時系結)
在Java中,呼叫某個類的方法,究竟執行了哪段代碼(是父類 or 子類方法的代碼),要看這個參考指向的是父類物件還是子類物件,這個程序是程式運行時決定的,而非編譯器,故稱為:運行時系結(也叫:動態系結)
舉例:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
System.out.println("Animal(String)");
}
public void eat(){
System.out.println(this.name + " " + "Animal::eat()");
}
}
class Cat extends Animal {
public int count = 66;
public Cat(String name){
super(name);//顯式呼叫,不是繼承
System.out.println("Cat(String)");
}
public void eat(){
System.out.println(this.name + "喵喵喵Cat::eat()");
}
}
public static void main(String[] args) {
Animal animal = new Cat("mimi");
animal.eat();
}
呼叫的是Animal(父類)的eat,運行時為Cat(子類)的eat,這個程序就成為運行時系結 (動態系結)
反匯編驗證:
- 找到對應目錄
.- 找到主函式,并與代碼結合
.
重寫 override
子類實作父類的同名方法,并且引數的型別和個數完全相同,這種情況稱為:覆寫 / 重寫 / 覆寫
重寫,即 外殼不變,核心重寫
之前的 方法篇 提到過重寫的概念以及重寫和多載的區別
重寫 多載 方法名稱 相同 相同 回傳值 相同 不做要求 引數串列 相同 不同(引數個數 / 引數型別) 類 不同的類(繼承關系上) 同一個類
重寫注意事項🔺
- static 修飾的靜態方法不能重寫,但是能夠被再次宣告
宣告為 final 的方法不能被重寫
構造方法不能被重寫 - 引數串列與被重寫方法的引數串列必須完全相同
- 重寫中,子類的方法的訪問權限不能低于父類的方法訪問權限
- 如果不能繼承一個類,則不能重寫該類的方法
- 私有的方法是不能被重寫的
坑來了~

在構造器中呼叫重寫的方法:在構造方法中,可以發生動態系結
先上代碼,便于后面理解
class Animal{
protected String name;
public Animal(String name){
this.name = name;
//System.out.println("Animal(String)");
eat();
}
public void eat(){
System.out.println(this.name + " " + "Animal::eat()");
}
private void sleep(){
System.out.println(this.name + " " + "Animal::sleep()");
}
}
class Cat extends Animal {
public int count = 66;
public Cat(String name){
super(name);//顯式呼叫,不是繼承
//System.out.println("Cat(String)");
}
public void mem(){
System.out.println(this.name +" " + "Cat::mem()");
}
public void eat(){
System.out.println(this.name + "喵喵喵Cat::eat()");
}
}
public static void main(String[] args) {
Cat cat = new Cat("米米");
//cat.eat();
}

在主函式里構造 Cat物件 時,會呼叫父類的構造方法,而父類的構造方法會呼叫eat,而最后列印的結果是子類中的eat
即:在構造器中呼叫重寫的方法,也會發生動態系結 (運行時系結)
- 構造 Cat 物件的同時,會呼叫 Animal 的構造方法
- Animal 的構造方法中呼叫了 eat 方法,此時會觸發動態系結,會呼叫到 Cat 中的 eat
盡量不要在構造器中呼叫方法(如果這個方法被子類重寫,就會觸發動態系結,但是此時子類物件還沒構造完成,可能會出現一些隱藏的但是又極難發現的問題
多型好處:
1.類呼叫者對類的使用成本進一步降低
- 封裝是讓類的呼叫者不需要知道類的實作細節,只管呼叫共有的方法即可,降低了代碼管理的復雜度
- 多型能讓類的呼叫者連這個類的型別是什么都不用知道,只需要知道這個物件具有什么方法即可
2.能夠降低代碼的"圈復雜度",避免使用大量的條件陳述句
多型總結:
拋開Java,多型是一個更廣泛的概念
- C++中的"動態多型"和 Java 中的多型類似
C++中的"靜態多型",就和繼承體系沒有關系了- Python 中的多型性體現的是"鴨子型別",與繼承體系沒有關系
- Go 語言中沒有"繼承"概念,同樣也可以實作多型
無論哪種語言,多型的核心都是讓呼叫者不必關注物件的具體型別,這也是降低用戶使用成本的一種重要方式
多型是面向物件程式設計中比較難理解的部分,會在后續的抽象類和介面中進一步體會多型的使用,重點是多型帶來的編碼上的好處
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297823.html
標籤:java




