java核心技術卷1 第五章:繼承

shueykkk發表於2024-03-17

學習重要的是出,而不是入,此前一直埋頭向前學,忽視了複習的重要性。寫一個部落格作為自己的學習筆記,也可作為以後查漏補缺的資料,溫故而知新。

類,超類和子類

  1. 一個繼承另一個類,父類也稱為超類,基類。"超類"中的超來自於集合理論,指的是父類,與之後的super關鍵字對應
  2. java中,類的繼承預設為public繼承(只有公共繼承),與c++不同。
  3. 子類public繼承父類,所以繼承到的欄位和方法的訪問許可權都不變,和c++同,子類中無法直接訪問父類的private成員(繼承到了,但是無法訪問)
  4. 子類中可以overrride父類方法,覆寫後直接呼叫使用的就是自己定義的方法。如在一個覆寫方法中直接呼叫父類同名方法會迴圈呼叫直到爆棧,應該加super
  5. super只是一個指示呼叫父類方法的關鍵字,而不是任何的物件引用
  6. 子類可以覆寫,增加欄位和方法,但不會刪除繼承到的欄位和方法

子類的建構函式

因為子類繼承了父類的欄位,例項化物件是需要初始化這些欄位,但是子類可能不能直接訪問這些欄位(private)
解決方法

建構函式中的第一句使用super(),內部填寫引數,對應父類的一個建構函式,這樣會呼叫父類的建構函式來初始化父類的那部分欄位。
如果不顯示呼叫super(),那麼會預設呼叫父類無參建構函式,這時候父類必須可以提供一個無參的建構函式

this和super

作為第一句出現:

this:在一個類的建構函式內的第一句呼叫,可以用來呼叫其他建構函式來初始化部分的欄位

super:子類建構函式第一句呼叫,初始化父類欄位

作為一個呼叫指示詞

this:指示當前的物件,可以用來獲得當前物件的欄位和方法
super:獲得父類的欄位和方法

多型

指的是一個型別的物件可以引用(指向,指示)多種不同的型別(子類),這種特性稱為多型

而在呼叫方法時可以根據實際指向的物件型別自動選擇對應物件的方法,稱為動態繫結


c++中的多型需要用virtual關鍵詞,而java中預設的物件賦值行為就是多型的

如果父類物件中的方法是final的,則不允許Override,從而實現動態繫結

繼承層次結構

繼承同一個父類的不同子類彼此是獨立的,並無直接關係

java不支援多繼承

c++支援一個類同時繼承多個父類,java不支援

繼承原則

只有兩個類滿足 is-a ,才適合用繼承。一個類是另一個類,只是更加特殊。

替換原則:is-a還指出:因為子類就是父類,所以任何需要父類的地方,都可以用子類替代,父類可以直接引用(指向)子類

父類可以透過引用子類,來呼叫對應的方法,實現不同的行為。

其中:

  1. 父類只能呼叫父類中定義的方法,因為這實際上是一個父類指標,只能看見父類的成員

  2. 只能把子類賦值給父類,而不能把父類賦值給子類。只能多->少,而不能少->多

  3. 進行方法呼叫時,會從所有的候選方法中選出最佳的呼叫。(引數匹配->引數型別相容...),若多個可能匹配則報錯

函式簽名

函式簽名(函式名和引數),返回值不是前面的一部分,只要函式名和引數一致,就判斷是相同方法

一個方法和另一個方法相同,是說函式名和引數一致。

當子類中定義了一個和父類函式名和引數相同的方法,就代表覆寫了父類的方法,而返回值不一樣一模一樣,只要現在的返回值型別和原本父類的型別相容,也算是合法的方法重寫。相容指的是可以給原本型別賦值的型別,比如子類到父類的相容

靜態繫結和動態繫結

靜態繫結:可以直接確定呼叫的是哪個物件的方法:private,final和static修飾的方法

private,子類中無法訪問,所以呼叫一定是父類的

final:不允許重寫的方法

static:屬於一個類的方法,因此透過類和物件呼叫一定是對應類中的方法

動態繫結:呼叫一個方法是,呼叫實際指向的哪個物件的方法,如果子類中重寫了,那麼呼叫子類的方法邏輯,資料使用子類中定義的欄位(相當於子類在呼叫),如果沒有重寫,那麼呼叫父類的方法

如果不是private,final,static修飾的方法,都將採用動態繫結的方式,實際呼叫的方法是實際引用的哪個物件型別的方法


warning

重寫父類的方法是,子類中的方法可見性不可以低於父類,只能寬鬆可見性,而不能設定更嚴格的可見性

阻止繼承

使用final關鍵字修飾class(放在class前):代表不允許派生子類

使用final關鍵字修飾方法,代表子類不允許重寫這個方法。

final修飾的類的所有方法自動稱為final方法。但是不包括欄位,因為欄位加上final代表是const,不可變,和final用了同一個關鍵詞,但是代表不同的含義

將類或方法宣告為final的原因:不希望子類來搗亂,這樣一個型別的物件引用的一定是對應型別的物件,而不會發生多型,呼叫不一樣行為的方法


強制型別轉換

可以在繼承層次結構內進行強制型別轉換(物件引用變數的強制轉換

  • 可以子類自然的轉成父類,比如直接賦值使用,但是父類轉子類需要強制轉換,且不一定成功

  • 轉換失敗會丟擲ClassCastException異常,不捕獲程式就結束了

  • 進行強制型別轉換的原因:要使用原本物件的完整功能,如把一個被父類指向的子類還原成子類,從而可以呼叫子類中新增加的方法

  • instanceof:用於判斷一個物件是否是另一個物件,是否is-a關係?前面的引用變數應該屬於後面的類是,繼承或進行了介面實現。比如一個父類物件引用指向了一個子類,如Person person = new Student();,那麼 person instanceof Student 是true,因為person物件雖然是父類,但是實際上指向子類,所以其是子類的instance,如果其指向父類,判斷就會返回falsePerson person = new Person();,person instanceof Student。

父類轉子類需要強制型別轉換,如果其是父類引用到子類,那麼將這個物件引用轉為子類可以成功。因為子類賦值給父類,父類實際上指向的是子類物件,但是由於型別,它的能力被限制了,只能訪問到父類定義的那部分,所以可以強制轉換成功,恢復全部的能力。

但是如果父類本身指向的是父類,而直接轉換為子類,就會報異常,是一個ClassCastException異常,因為父類實際上沒有定義子類那部分成員,所以轉換失敗

instanceof:

  • 子類物件 instanceof 父類,返回true,子類是父類的例項,is-a
  • 父類物件 instanceof 子類,看父類物件的引用物件,如果引用對應的子類,返回true,可以強制轉換成對應子類恢復全部能力,如果不是,返回false

受保護訪問

protected:一個包下可以訪問,或是其他包的子類可以訪問。

應當慎用保護,或是乾脆不用。

比如使用protected欄位,其他類繼承後使用,可以直接訪問到類的具體細節,他們的實現就會依賴這個欄位,如果你決定重寫,那麼其他人的程式碼可能會變得不可用

相比之下,protected修飾方法顯得更有意義一些,只讓那些很 熟悉父類實現的子類可以訪問到這個方法,而其他的類不可以。(但是同一個包下的類還是可以訪問到,所以protected修飾符意義並不大,少用)


Object:所有類的父類

object是所有類的父類

java中,只有數值,字元和布林型別不是物件,是屬於基本型別,primitive type,而其他所有的都是物件,比如陣列,無論是基本型別的陣列還是物件陣列,都是Object類的子類

c++與之類似的概念是,所有指標都可以轉換為void*指標,而沒有一個所有類的基類

equals方法

Object的equals方法,比較兩個物件的地址是否相同,很簡單,因此很多類都需要重寫這個方法來實現比較的邏輯

其中:

  • 一個物件為null,則equals返回fase
  • 兩個物件都是null,則返回true
  • 比較物件,需要先呼叫父類的equals方法,父類的欄位相等之後再繼續比較子類的欄位

其中,java語言規範要求實現的equals方法有以下性質

  1. 自反性:任意非null物件x,x.equals(x)應該返回true
  2. 對稱性:x.equals(y)和y.equals(x)相同
  3. 傳遞性:x.equals(y),y.equals(z),那麼x.equals(z)也為真
  4. 非null引用x.equals(null),返回false

當屬於同一個類的物件比較,這很自然。但當兩個比較的物件不是同一個類時,會變得很麻煩

  • 使用getClass,instanceof來判斷是否相等,可能會造成對稱性被違反,比如父類和子類,交換次序執行getClass,instanceof返回結果是不同的
  • 因此,對於不同類的物件進行equals比較時,很麻煩,很多類庫API實現都沒有做到對稱且正確

Override註解

告訴編譯器,這裡的方法是在重寫父類的方法,如果編譯器發現其實是在定義新方法而不是重寫, 就報錯

hashcode方法

如果x.equals(y)為true,那麼這兩個物件呼叫hashcode方法返回的因該是相同的int值。

hashcode:雜湊碼,如果物件不同,那麼hashcode值也基本不會相同

hashcode返回一個int,可以是負數

預設Object類實現的hashcode基於地址算出,如果物件地址相同,hashcode就相同

String類重寫了hashcode方法,因為equals根據字串內容判斷,如果字串內容相同那麼相同,這時候的hashcod也相同,而不是根據地址判斷

Object類提供了一個hash方法,可以接收多個引數,返回這些引數hashcode的組合,得到一個組合所有引數欄位後的hashcode

基本資料型別的hashcode:透過其對應的包裝類:Double,Integer等類的hashcode方法得到

toString方法

類方法中應該定義一個toString方法,列印物件的狀態資訊,幫助使用者理解物件狀態

當呼叫println列印物件,就會呼叫這個類的toString方法,當物件透過+與一個字串拼接時,java編譯器也會自動呼叫toString方法獲得物件的字串描述

如果不重寫, 預設會有一個toString方法,這裡本機測試是:類名@hashcode

應該可以透過getClass.getName()方法獲得物件的類名

許多類庫中定義的類都有toString()方法,幫助我們獲得和物件狀態有關的資訊

強烈建議每個類定義一個toString,方便自己和使用者

ArrayList,泛型陣列列表

一個可變大小的陣列,其定義為泛型。

宣告:

  1. 其中左邊的<>中填入物件型別,不能是基本數值型別,基本數值型別需要用對應的包裝類
  2. 右邊的<>預設是Object,不用填入內容,填入也會被忽略
  3. ()中可以填入容量,這裡的容量是可以隨著增加刪除動態改變,但是容量不夠時需要開闢空間,然後複製內容,比較耗時,因此儘量選擇合適的容量
  4. 如ensureCapacity方法,可以指定一個容量,讓建立的陣列列表可以包含有這麼多的容量,避免後面容量不足的耗時操作
  5. trimToSize方法,可以削減容量,由垃圾回收器收回多餘的儲存容量,而只儲存現有的元素儲存空間

'''

public class Hello {
    public static void main(String[] args) {
        ArrayList<Person> people=new ArrayList<>();
        people.add(new Student());
        people.add(new Employee());
        for(var person:people)
            System.out.println(person.getName());

    }
}

'''

使用:

  1. 定義一個容量
  2. 透過add增加元素,透過set更改已增加元素的值,透過get獲得指定索引的值
  3. 容量!=size,size是實際包含的元素數量,set,get也只能改變和獲得已經add進來的元素,而不能是其他的元素

注意:

ArrayList類似於C++的vector,但是沒有過載[],只能透過get(index)訪問元素

其他問題

  • 插入和刪除的效率低,如用add(index,e)和remove(index)方法,內部的元素都要移動來保持順序儲存,類似於順序線性表的性質
  • 支援for each的迴圈訪問
  • set(index,e)替換內容後返回原本那個位置的內容

物件包裝器與自動裝箱

基本型別的資料需要轉換為物件是,用到的是其對應的物件包裝器類。

有Integer,Long,Double,Float,Short,Byte,Character,Boolean。對應不同的基本資料型別,其中,前面的六個繼承了Number公共類。

這些包裝器類的性質

  • 不可變類。也就是其中對應的基本型別值確定後就不可以改變,final
  • final類,不允許派生
  • ArrayList<int>不允許,因為型別需要是物件,所以要使用包裝器類

自動裝箱與拆箱

使用基本型別和使用包裝類很一致,需要包裝類的地方直接填入基本型別也可以,如ArrayList<Integer>.add方法,可以直接填入ArrayList.add(7),這會自動改寫為ArrayList.add(Integer.valueOf(7)),而把一個Integer物件賦值給一個int,會自動拆箱,變成int型。類似於呼叫了Integer.intValue().

大多數情況下,基本可以認為,基本型別和他們的包裝類是一樣的

一些例外:

  • 如ArrayList<>中填型別,一定要是包裝類
  • 作比較時,包裝器類作為物件比較的是地址,需要呼叫equals方法才能正確比較數值

一些注意點:

  • 構造包裝器物件:呼叫valueOf方法,來賦值,而不是用new Integer(7)這樣的,這種嗲用構造器的方法從version9就被廢棄了
  • 裝箱和拆箱是編譯器的工作,當其產生位元組碼後,相關的操作已經被完成了,虛擬機器只負責執行這些位元組碼
  • 一些很有用的方法被放在了包裝類中,比如將字串轉為int,呼叫Integer的parseInt方法就可以,傳入一個String可以轉為int
  • 包裝器的一些方法:
    1. Integer的static方法:toString(int i,int radix),可以傳入兩個引數,來將一個數值轉為對應進位制的字串,比如傳入8,就可以把這個8傳喚為2進位制的1000字串
    2. parseInt:可傳入兩個引數,表示要用多少進位制來解析這個字串到十進位制int值
    3. valueOf:初始化一個Integer,可以是int值,也可以傳入String,傳入String可以一個引數或者兩個引數,兩個引數就可以再指定一個進位制,表示用多少進位制來解析這個字串到int

引數個數可變的方法

可以提供可變的引數數量給方法

使用:型別之後加上... ,表示可接受多個此型別的引數,需作為最後一個引數.

public void hello(int i,String... strings){}

這裡傳入多個String之後,會儲存在strings[]陣列中,用strings[]陣列訪問傳入的數量可變的引數

  • 因為傳入的引數會被放入一個陣列中,因此使用...可變引數和使用陣列作為引數是等價的
  • 傳入可變個引數,也可以直接傳入一個陣列物件,這樣進入方法後也可以訪問到陣列的各個元素,等價於傳入了可變個引數
  • 如果一個方法的最後一個引數是陣列,那麼其就可以直接替換為...可變引數,使用完全等價
  • 如Main函式,就可以宣告引數為String... args

#補充

當需要接受物件引數時,如Object[],如果傳入基本資料型別,則會自動裝箱為對應的包裝類

只有實際的資料可以被包裝,型別不會自動轉換,如ArraryList<int>不會自動變為ArraryList<Integer>,而add加入引數時會把一個7自動包裝為Integer.valueOf(7)


抽象類

將一些公共的欄位和方法提取,作為父類,其中可以有一些方法不做實現,作為佔位符,等待子類繼承後實現。這種父類中不做具體實現的方法稱為抽象方法,abstract方法,為了程式清晰,有抽象方法的類必須宣告為抽象類,abstract class

  • 抽象類中可以包含具體的欄位和方法
  • 子類中可以實現抽象方法。如果不實現,那麼子類也需要宣告為抽象的
  • 不含有抽象方法,也可以宣告類為抽象類
  • 抽象類不能例項化,不可以建立抽象類的物件,即不可以new,構造出一個真正的物件
  • 可以建立抽象類的引用變數,指向具體實現的子類,從而實現多型
  • 一個經典用法是建立父類陣列,其中的值均為子類的物件,體現子類is-a父類關係

密封類(mark,使用場景暫不清楚,專案場景下理解)

java15預覽特性,java17最終確定

密封類可以控制哪些類可以繼承它:保證自己控制繼承體系,保持完整功能,而不被外部隨意繼承打亂

public abstract sealed class JSONValue
	permits JSONArrary,HSOONNumber
{
...
}

這裡sealed關鍵字表示密封類,permits表示允許繼承的子類

  • 允許的子類必須可見,可訪問,不可是一個類的內部私有類,不允許是一個包內的包內訪問類
  • 允許的公共public子類,必須和密封類一個包下
  • 可以不加permits語句,但是所有可以繼承的子類都要和密封類一個檔案下

允許繼承的子類:必須是final,sealed或是non-sealed

  • final代表子類不允許繼承
  • sealed表示允許指定的類繼承
  • non-sealed表示允許所有的繼承

non-sealed:第一個帶連字元的java關鍵字

  • 連字元可能帶來一些風險,導致現有程式碼無法編譯,比如定義一個int non和int sealed變數,可以計算non-sealed,相減得到一個結果,而再jdk17版本中這個表示式會被解析為一個關鍵字
  • 連字元可能會成為java關鍵字的一個趨勢

反射

暫時略,寫java開發工具使用多,關注應用程式開發的使用較少

繼承的設計技巧

  1. 公共的欄位和方法提取作為父類,而不子類中多次重複定義

  2. 不適用protected保護欄位

    • 意義不大,因為並不能提供太多保護機制
    • 子類是隨意派生的,任何人任何地方都可以派生一個子類,訪問protected欄位,破壞封裝性
    • 同一個包下,即使不是子類也可以訪問protected欄位,破壞封裝性
  3. 不濫用繼承,只有是is-a關係再使用繼承,如果不是,將其作為平級的獨立類而不是作為繼承關係

  4. 利用多型實現基於型別的不同動作,而不是用if-else判斷型別後指向不同的動作

相關文章