詳解Java的繼承機制和繼承的內部處理

一、繼承機制

       子類會自動繼承父類的成員供自己使用,但有時候該成員可能不符合子類的要求。一直簡單的解決辦法是不使用它,另外取名定義新的奕量和方法。 但有時取名是一件麻煩事,而且子類的使用者有可能在無意中使用了設計者不願意提供的成員(因爲子類沒有修改它的訪問權限)。一種徹底的解決辦法是爲成員取一個同樣的名字,並重新定義它的值或行爲,將父類的同名變量和方法遮蓋掉。
       子類的成員變量和父類的同名,稱爲父類的成員變量(屬性)被隱藏;如果是成員方法同名,稱爲父類的成員方法被覆蓋。

1.屬性的隱藏

只要子類中的成員變量與父類的同名,就可以將父類的變量隱藏起來,一般情況下使用的就是子類的同名變量,不過有些細節還是要注意,比如訪問權限修飾符,常量修飾符,靜態修飾符、數據類型說明符等,Java允許這些修飾符不同。下面講解幾種情況

       1.修飾符完全相同的情況:
       這個無需多解釋,各類修飾符完全相同的隱藏就是最基礎的。

       2.訪問權限不相同的情況:
       Java中規定,子類用於隱藏的變量可以和父類的訪問權限不同,如果訪問權限改變,則以子類的權限爲準。

       3.數據類型不相同的情況:
       Java允許子類的變量和父類變量的類型完全不同,以修改後的數據類型爲準。

       4.常量修飾符不同的情況:
       Java允許父類的變量被子類的常量隱藏,也可以是父類的常量被子類的變量隱藏。

       5.靜態變量修飾符不同的情況:
       Java允許用實例成員變量來隱藏靜態成員變量,也允許以靜態成員變量來隱藏實例成員變量

綜上這些概括起來就是:子類變量可以修改繼承下來的父類變量的任何屬性,使用子類對象時,以修改之後的屬性爲準。

2.方法的覆蓋

子類中,如果繼承下來的方法不能滿足自己的需要,可以將其重寫一遍,這稱爲
覆蓋”(也叫重寫)。覆蓋必須滿足以下兩個條件。
  1.方法名稱必須相同。
  2.方法的參數必須完全相同,包括參數的個數、類型和順序
如果只滿足第一條,不滿足第二條,那麼就不是覆蓋,而是重載。由於方法不僅有各種權限修飾符,而且還有返回類型修飾符,所以它的覆蓋比成員變量的隱藏規則要複雜一些。**原則上,如果覆蓋成功,那麼使用子類對象時,方法的所有屬性都以覆蓋後的爲準。**下面分類來介紹覆蓋時的一些要求。

       1.修飾符完全相同的覆蓋:
       最基本的,也是最簡單的。

       2.訪問權限不相同的情況:
       子類方法的訪問權限可以與父類的不相同,但只允許權限更寬鬆,而不允許更嚴格,它遵循的是“公開的不再是祕密”這一原則,沒有任何辦法能夠改變這一原則,這一點和C++有很大的區別。

       3.返回值數據類型不相同的情況:(這條是可以區別重寫和重載的)
  如果返回的是基本類型,那麼在覆蓋時不允許出現返回值數據類型不相同的情況。也就是說,覆蓋與被覆蓋的方法的返回值數據類型必須完全相同。
  如果返間類型是複合類,時問類型必須相容。也就是說,或者完全相同,或者子類的返回類型與父類的返回類型存在繼承關係。

       4.final修飾符不同的情況:
  若方法前面用final修飾,表示該方法是一個最終方法,它的子類不能覆蓋該方法。反之,一個非最終方法可以在子類中指定final修飾符,將其變成最終方法。

       5.靜態修飾符不同的情況:
  Java規定,靜態方法不允許被實例方法覆蓋,同樣,實例方法也不允許用靜態方法覆蓋。也就是說,不允許出現父類方法和子類方法覆蓋時的static修飾符發生變化。

3.構造方法沒有繼承機制

從形式上看,構造方法比普通方法要簡單:它沒有返回值,沒有 static、final等x修飾符,而且一般不會用 private修飾。按照某些教材的說法,構造方法也如普通成員方法一樣可以被繼承,只是有一些特殊性。但是Sun公司在《Java語言規範》中明確規定構造方法不是成員方法,所以它不遵循成員方法的繼承規則,而且它根本不會被繼承。

       1.無參數構造方法的自動調用機制
       按照Sun公司的解釋,系統是自動爲子類添加了一個不帶參數的構造方法,而子類這個構造方法又會自動調用父類無參數的構造方法。這種調用就是通過super關鍵字來實現的。
在這裏插入圖片描述
這裏是自動調用父類的無參構造方法,即使我們不加super,系統也會默認加上

       2.帶參數的構造方法不會被自動調用
       系統只提供不帶參數的構造方法,沒有提供帶參數的構造方法,自然無法調用帶參構造方法,只能顯式的使用super進行調用
在這裏插入圖片描述
這裏我註釋掉就會報錯

下面這個就可以綜合體現這兩點,第一,這個是會報錯的,原因是無參數構造方法的自動調用機制,會給紅色框那裏加上super()方法,調用父類的無參構造,但是父類沒有無參構造,所以報錯。第二,那個藍色框那裏體現了帶參數的構造方法不會被自動調用這個原理,只能顯式的使用super調用
在這裏插入圖片描述
最後還要明確一點,由於構造方法不會被繼承,也就不存在覆蓋的問題。Java還規定,子類中無論哪個構造方法在執行時,都會先執行父類中無參數的構造方法,除非顯示地調用了其他的構造方法。(就像上面那個例子的藍色框裏面實現的一樣)



二、繼承的內部處理

       對於父類,當它被子類繼承後,並非複製了一份成員方法和成員屬性到子類的空間中,它仍然只在父類空間中存在一份,子類通過繼承鏈(本質上是指針) 來訪問父類中的方法。如果程序中通過“子類對象名.成員方法名” 的方式使用成員,編譯器會首先到子類中查找是否存在此成員,如果沒有,順着繼承鏈到其父類空間中查找,依次往上推,如果找到Ob-ject類(該類爲所有類的公共祖先)還未發現此成員,則編譯器報錯。

       由於父類的成員沒有被複制到子類空間中,所以系統在生成子類對象時,會自動先生成一個父類的隱藏對象,父類如果還有父類,則以此類推。這就是爲什麼我們調用子類構造方法會自動添加 super()方法(即父類的構造方法),正是用於實現父類對象的構造。如果有多個父類對象,則所有對象共享一份成員方法,每個對象有各自的存儲空間保存成員屬性。
  
  在自動生成父類對象的過程中,必須保證父類的 class 文件可以訪問到。 如果編譯成功後,將父類的class 文件刪除,運行時系統將會報錯。

(這裏可以小小的插一段我自己的理解,關於繼承的話,有時候很多調用父類的方法以及使用變量其實都是對父類的那個隱藏對象在進行操作,本質上是子類對象通過繼承鏈訪問或者修改父類對應的那個隱藏對象,很多人就迷惑在這個地方,子類對象跟父類對象是不能混淆的(重寫父類方法或者變量的話就是自己所擁有的了),只不過我們可以拿這個隱藏父類對象的一切,這就是繼承)

下圖爲所示系統內部對繼承的處理方法。
在這裏插入圖片描述

       從圖中可以看出,即便成員變量b已經被覆蓋,但只要調用方法f(),則仍然可以訪問到被覆蓋的父類變量b。(一般這個就是通過super實現)

詳細瞭解繼承的內部處理,下面我們來舉幾個例子
1.隱藏對象的講解

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	public int m = 2;
}

如果不是很清楚Java是如何實現繼承的話,這個是很容易做錯的,慣性思維會認爲輸出結果是2,但是我們可以很清楚的判斷,B調用的是父類方法view。 方法中的this指代了當前的父類A創建的隱藏對象,所以調用的是該對象的m,輸出結果應該是1。

運行結果:
在這裏插入圖片描述


2.同樣的代碼,我們改一下,我們重寫一下父類的方法

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	public int m = 2;
	public void view() {
        System.out.println(this.m);
    }
}

運行結果:
在這裏插入圖片描述
這次結果爲2,原因就是我們重寫了父類方法,子類已經擁有了,就不會去父類查找了,所以調用的是自己的方法,this指代的是當前B類創建的對象b,所以結果爲2。


3.最後我們驗證一下如果子類不存在成員變量之類的話,會不會去查找父類是否存在

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	//public int m = 2;
	public void view() {
        System.out.println(this.m);
    }
}

運行結果:
在這裏插入圖片描述
很明顯,輸出結果爲1,驗證了我們的猜想。


當然我們上面所有的只是測驗了一下成員變量,而對於成員方法而言,又是不是這樣的呢?答案可能是出乎你的意料(具體詳情可以瞭解一下Java靜態綁定和動態綁定機制,相信這些你一定會更加喫透)

推薦一篇閱讀:https://blog.csdn.net/weixin_43465312/article/details/101542326

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