1. 程式人生 > >JAVA核心技術筆記總結--第5章 繼承

JAVA核心技術筆記總結--第5章 繼承

指定 方法重載 阻止 決定 基礎 驗證 cep 兩個類 屏蔽

第五章 繼承

繼承是指基於已有的類構造一個新類,繼承已有類就是復用(繼承)這些類的成員變量和方法。並在此基礎上,添加新的成員變量和方法,以滿足新的需求。java不支持多繼承

5.1 類、超類和子類

5.1.1 定義子類

下面是由繼承Employee類來定義Manager類的格式,關鍵字extend表示繼承。

public class Manager extend Employee{
  //添加方法和成員變量
}

extend 表明定義的新類派生於一個已有類。被繼承的類稱為超類、基類或父類;新類稱為子類或派生類。

在Manager類中增加了一個用於存儲獎金信息的域,以及一個用於設置這個域的新方法:

public class Manager extend Employee{
  private double bonus;
  ...
  public void setBonus(double bonus){
    this.bonus = bonus;
  }
}

盡管在Manager類中沒有顯式的定義getNamegetHireDay等方法,但Manager類的對象卻可以使用它們,這是因為Manager類自動繼承了超類Employee中的這些方法。此外,Manager類還從父類繼承了name、salaryhireDay這3個成員變量。從而每一個Manager對象包含四個成員變量。

5.1.2 覆蓋方法(Override)

子類除了可以定義新方法外,還可以覆蓋(重寫)父類的方法。覆蓋要遵循 "兩同兩小一大"的原則:

兩同指:

  • 方法名相同。
  • 形參列表相同,即形參個數相同,各形參類型必須對應相同(新方法的形參類型不可以是父類方法形參類型的子類型)。

兩小指:

  • 子類方法返回值類型應與父類方法返回值類型相同或是父類方法返回值類型的子類型。
  • 子類方法聲明拋出的異常應與父類方法聲明拋出的異常類型相同或是其子異常。

一大指:子類方法的訪問權限應與父類方法的訪問權限相同或比父類訪問權限大。

從此處可以看出,覆蓋和重載的區別:

方法重載對返回值類型和修飾符沒有要求;實例方法可以重載父類的類方法,而方法覆蓋要求返回值類型變小,修飾符變大且只能為實例方法

,滿足上述條件的靜態方法,不叫覆蓋,只是屏蔽。

尤其要指出的是:滿足覆蓋條件的方法要麽都是類方法,要麽都是實例方法,不能一個是類方法,一個是實例方法,否則會引發編譯錯誤。

編譯時,編譯器會為每個類建立一張方法表,表中包含的方法有:子類中定義的所有方法(包括私有方法,構造器,類方法和公有方法)。同時包含從父類繼承的非私有方法,包含重載的方法,應該也包含類方法,但不包含被子類覆蓋的方法。此外,編譯器也會為每個對象分配一個成員變量表,其中包含本類中定義的所有變量(成員變量和類變量),以及父類中非私有的變量。

當子類覆蓋了父類方法後,子類對象將無法訪問父類中被覆蓋的方法,但子類方法中仍能調用父類中被覆蓋的方法,調用格式為:

  • "super.方法名" (被覆蓋的是實例方法),僅可以在子類非靜態方法使用。編譯時,編譯器會根據super去查找父類的方法表,並替換為匹配方法的符號引用。
  • "父類類名.方法名"
public class Manager extend Employee{
  private double bonus;
  ...
  public double getSalary(){
    return super.getSalary() + bonus;
  }
}

如果父類方法的訪問權限是private,則子類無法訪問該方法,且無法覆蓋該方法,當子類中定義了一個與父類 private方法滿足上述”兩同兩小一大“原則的方法時,並沒有覆蓋該方法,而是在子類中定義了一個新方法。例如:

class BaseClass{
  //test()方法是private訪問權限,子類不可訪問
  private void test(){...}
}
class SubClass extends BaseClass{
  //此處不是覆蓋,所以可以增加static關鍵字
  public static void test(){...}
}

從而,子類可以繼承的內容有

  • 父類的成員變量(對於父類的私有成員變量,子類對象依然分配有內存,僅因為子類對象不能直接訪問)。
  • 父類的非私有方法。

不能繼承的有:

  • 父類中被覆蓋的方法,不能繼承。
  • 父類的私有方法。
5.1.3 super限定

super用於限定該對象調用它從父類繼承得到的實例變量或方法。super不能出現在static修飾的方法中

如果在構造器中使用super,則super用於初始化從父類繼承的成員變量。

對於非私有域而言,如果子類定義了和父類同名的實例變量,則會發生子類實例變量隱藏父類實例變量的情形。默認情況下,子類裏定義的方法直接訪問的同名實例變量是子類的實例變量。但可以在使用"super.實例變量名"來訪問被隱藏的父類的實例變量。例如:

class BaseClass{
  public int a;
}
class SubClass extends BaseClass{
  public int a = 7;
  public void test1(){
    System.out.println(a);
  }
  public void test2(){
    System.out.println(super.a);
  }
}

如果子類中沒有與父類同名的成員變量,那麽在子類實例方法中訪問該成員變量時,無須顯式使用super或父類名。如果某個方法中訪問了名為a的實例變量,但沒有顯式指定調用者,則系統查找a的順序為:

  • 查找該方法中是否有名為a的局部變量
  • 查找當前類是否有名為a的成員變量
  • 查找直接父類中是否包含了名為a的成員變量,依次上溯所有父類,直至Object類,如果沒有找到,則報錯。

當程序創建一個子類對象時,系統不僅會為子類中定義的實例變量分配內存,也會為從父類繼承的所有實例變量分配內存,即使子類中定義了與父類中同名的實例變量。

由於子類的實例變量僅是隱藏了父類中同名的實例變量,不是覆蓋。所以,訪問哪個實例變量是由調用者的類型決定的。在編譯時,由編譯器分派。從而,會出現如下情形:

class Parent{
  String tag = "parent";
}
class Child extends Parent{
  private String tag = "child";
}
public class Test{
  public static void main(String[] args){
    Child c = new Child();
    //報錯,不可訪問私有變量
    out.println(c.tag);///////1
    //輸出:parent
    out.println(((Parent)c).tag);//////2
  }
}

當程序在代碼1處試圖訪問tag時,由於調用者為子類,而子類的tag是私有變量,不能在外部被訪問.。而代碼2處訪問的是父類的tag。此與方法的覆蓋不同,方法覆蓋具有多態性。

綜上,當子類中隱藏了父類的實例變量,或子類中覆蓋了父類的方法時,父類被隱藏的實例變量,或被覆蓋的方法,僅能在子類的方法裏面通過super來訪問,在其他類的方法中無法訪問。

5.1.4 子類構造器

由於子類不能訪問父類的私有域,所以需要利用父類的構造器來初始化這部分私有域,可以通過super實現對父類構造器的調用。使用super調用構造器的語句必須是子類構造器的第一條語句。

不管是否使用super顯式的調用了父類構造器,子類構造器總會調用一次父類構造器,子類調用父類構造器分如下幾種情況:

  • 子類構造器執行體的第一行使用super顯式調用了父類構造器,系統將根據super調用傳入的實參列表調用父類構造器。
  • 子類構造器執行體的第一行代碼使用this顯式調用本類中重載的其他構造器,系統將根據this調用裏傳入的實參列表調用本類的另一個構造器。執行本類中另一個構造器時即會調用父類構造器。
  • 子類構造器執行體中既沒有使用super調用,也沒有使用this調用,系統將會在執行子類構造器之前,隱式調用父類的無參數構造器。(所以,定義類時,最好提供一個無參數構造器)。

綜上,調用子類構造器時,父類構造器總會在子類構造器之前執行。以此類推,執行父類構造器時,系統會再次上溯執行其父類的構造器.....從而,創建任何java對象,最先執行的總是Object類的構造器。當父類有構造器,但不存在默認構造器時,程序出錯。

實際上初始化塊是一個假象,源文件編譯時,初始化塊中的代碼會被"還原"到每個構造器中,且位於構造器所有代碼的前面。(super()調用的後面,this調用的前面??)。

在創建子類對象時,各模塊的初始化流程如下:

  1. 在未執行任何初始化語句時,系統已經為子類的所有成員變量,以及父類的所有成員變量分配了內存空間,並默認初始化,當子類中存在成員變量隱藏父類成員變量情況時,這兩個成員變量都會被分配內存。
  2. 然後初始化父類(先執行初始化語句和初始化塊,再執行父類構造器,若子類構造器中有super調用父類構造器語句,則調用指定的父類構造器,否則調用父類的默認構造器。
  3. 按序執行子類的顯式初始化語句或初始化塊。
  4. 執行子類構造器內的語句。

示例如下:

public class Test{
  {
    a = 6;
  }
  int a = 9;
  public static void main(String[] args){
    //輸出為9;若調換兩個初始化語句,輸出為6
    out.println(a);
  }
}

在首次使用子類時,類的初始化流程為:

若父類未初始化,則執行父類的類初始化,在執行父類初始化時,也要先判斷其父類是否已初始化,若未初始化,則先執行其父類的初始化,……直至Object類,最後才執行本類的類初始化。

5.2 多態

Java中引用變量有兩個類型:編譯時類型和運行時類型。編譯時類型由變量定義的類型決定,運行時類型由該變量引用對象的類型決定。如果編譯時類型和運行時類型不一致,就可能出現多態(PolyMophism)。

因為子類是一種特殊的父類,所以Java允許把一個子類對象直接賦給一個父類變量,由系統自動完成,無須類型轉換,稱為向上轉型。但不能將父類對象直接賦給子類變量。

兩個同類型的變量,若一個引用父類對象,另一個引用的是子類對象,且子類中覆蓋了父類的方法,那麽兩個變量同時調用此方法時,將呈現出不同的行為特征,這被稱為多態。

當父類變量引用子類對象時,由於變量的編譯時類型為父類,所以只能調用父類的方法和成員變量,以及子類中覆蓋的方法(動態連接、動態綁定),不能調用子類中新定義的、以及父類中存在,但子類重載後的只屬於子類方法。符號引用在編譯時分派。

與方法不同的是,對象的實例變量不具備多態性。總是訪問編譯時類型中定義的實例變量。

在繼承鏈中對象方法的調用存在一個優先級:????

this.show(O),super.show(O),this.show((super)O),super.show((super)O)

警告,在java中,子類數組的引用可以賦給父類數組的引用,而不需要強制類型轉換。例如:

Manager[] managers = new Manager[10];
//將它賦給Employee[] 數組是完全合法的:
Employee[] staff = managers;

因為managers[i]是一個Manager,可以賦給Employee變量,但是編譯器不允許讓數組元素再引用其他類型的對象,不允許數組元素引用的類型不一致。如下面語句將會拋出ArrayStoreException異常:

staff[0] = new Employee(...);

因為如果允許staff[0] 和 managers[0] 都引用了這個Employee 對象,那麽managers[0].getBonus()變的不合理。

即數組元素只能引用相同類型的對象,否則編譯器會報告異常。

理解初始化流程和多態性的一個極好的例子:

public class Mytest {
    public static void main(String[] args){
        Dervied td = new Dervied();
        td.test();
    }
}
class Dervied extends Base {
    private String name = "dervied";
    public Dervied() {
        super();
        tellName();
        printName();
    }
    public void tellName() {
        System.out.println("Dervied tell name: " + name);
    }
    public void printName() {
        System.out.println("Dervied print name: " + name);
    }
}
class Base {
    private String name = "base";
    public Base() {
        tellName();
        printName();
    }
    public void tellName() {
        System.out.println("Base tell name: " + name);
    }
    public void printName() {
        System.out.println("Base print name: " + name);
    }
    public void test(){
        tellName();
    }
}
輸出結果為:
//前兩句輸出,說明Base類中的tellName()和printName()仍然調用的是Dervied類的方法
//同時也說明,初始化流程為:先調用父構造器,而不是先執行顯式初始化語句:private String name = "dervied";
Dervied tell name: null
Dervied print name: null
//三、四兩句說明調用完Base類構造器後,初始化流程為:先執行顯式初始化語句,然後再執行構造器中的語句。
Dervied tell name: dervied
Dervied print name: dervied
//最後一句再次驗證了,Base類中的tellName()和printName()已經徹底被覆蓋
Dervied tell name: dervied

5.3 理解方法調用

假設要調用x.f(args),下面是調用過程的詳細描述:

  1. 編譯器查看對象的編譯時類型類型和方法名。假設隱式參數 x 的編譯時類型為 C 類。需要註意的是:有可能存在多個名字為 f,但是形參列表不同的方法。例如可能存在方法 f(int)和方法 f(String)。編譯器從方法表中列舉出所有方法名為f的方法,包括父類中非私有的且名為 f 的方法。至此,編譯器已獲得所有可能被調用的候選方法。

  2. 接下來,編譯器將查看調用方法時提供的實參類型。如果在所有名為 f 的方法中存在一個與提供的參數類型完全匹配,就選擇這個方法。這個過程被稱為重載解析(overloading resolution)。由於允許類型轉換(int 可以轉換成 double,子類轉換成父類,等等),所以過程很復雜,如果編譯器沒有找到與參數類型匹配的方法,或者發現經過類型轉換後有多個方法與之匹配,就會報錯。至此,編譯器已確定需要調用的方法,會將其替換為對應方法的符號引用。

5.4 阻止繼承:final 類和方法

不允許繼承的類被稱為 final 類。在定義類時使用 final 修飾符,就表明這個類是 final 類,不能被繼承,聲明格式如下:

public final class Executive{
  ...
}

類中特定的方法也可以被聲明為 final。被 final 修飾的非私有方法不能被子類覆蓋,但是方法仍然可以被重載(因為重載不考慮修飾符)。子類中可以定義父類中被final修飾的私有方法。

域也可以被聲明為 final,final 域表明域為常量。當一個類被聲明為 final 時,只是其中的方法變為 final,不包括域。

5.5 強制類型轉換

引用變量只能調用編譯時類型中的方法,不能調用運行時類型中定義的方法。如果需要調用運行時類型中的方法,必須進行類型轉換,將它強制轉換成運行時類型。引用類型轉換的語法和基本類型的強制轉換相同。但是,如果父類變量實際類型是超類時,強制類型轉換會引發ClassCastException。因此,可以在進行類型轉換之前,先使用instanceof檢測是否能進行轉換:

if(staff[1] instanceof Manager){
  boss = = (Manager) staff[1];
  ...
}

如果檢測返回false,編譯器就不會進行轉換。

綜上所述:

  1. 只能在繼承層次內進行類型轉換。
  2. 在將超類轉換成子類之前,應該使用 instanceof進行檢查。

只有在使用子類特有的方法時才需要進行類型轉換。建議盡量少用到類型轉換和 instaceof 運算符。

5.6 instanceOf運算符

instanceOf運算符的前一個操作符通常是一個引用類型變量,後一個操作符是一個類或接口instanceOf用於判斷前面引用變量的運行時類型是否是後面類或者其子類的實例。如果是,則返回true,否則返回false

使用instanceOf運算符時,要求前面的操作數的編譯時類型與後面操作數的類型相同,或者前者是後者的父類,或者前者是後者的子類。否則會引起編譯錯誤。

5.7 繼承與組合

5.7.1 使用繼承的註意點

允許子類繼承父類,子類可以直接訪問父類的成員變量和方法。但是繼承破壞了父類的封裝性:子類可以通過覆蓋的方式改變父類方法的實現,從而導致子類可以惡意篡改父類的方法。並且,父類方法調用被覆蓋方法時,調用的其實是子類的方法。

為了保證父類良好的封裝性,設計父類通常應該遵循如下規則:

  • 盡量隱藏父類的內部數據,盡量把父類的所有成員變量設置為private
  • 不要讓子類可以隨意訪問、修改父類的方法。父類中作為輔助的方法應設置為private。父類中需要被外部類調用的方法,必須設置為public,如果不希望子類重寫該方法,可以用final修飾;如果希望父類的某個方法被子類重寫,但不希望被其他類自由訪問,則可以使用protected來修飾。
  • 盡量不要在父類構造器中調用將被子類重寫的方法,容易出現邏輯混亂。
5.7.2 組合

組合是將待復用的類當成另一個類的一部分,即在新類中,定義一個待復用類的私有成員變量,以實現對類方法和成員變量的復用。例如:

class Animal{
  public void breath(){
    System.out.println("吸氣,吐氣。。。");
  }
}
class Bird{
  private Animal a;
  public void breath(){
    a.breath();
  }
}

組合和繼承都可以復用指定類的方法以及成員變量。繼承更符合現實意義。組合可以避免破壞父類的封裝性。

5.8 受保護的訪問

如果希望超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類的某個域,此時,需要將這些方法或域聲明為 protected

但是,將父類的域聲明為 protected後,子類的方法只能訪問子類對象的 protected 域,不能訪問父類對象的 protected 域。這種限制有助於避免濫用受保護機制,使得子類只能獲得受保護域的權利。

歸納 java用於修飾成員變量或方法的控制可見性的4個訪問修飾符:

  1. 僅對本類可見 -- private
  2. 對所有類可見 -- public
  3. 對本包和所有子類可見 -- protected
  4. 僅對本包可見 -- 默認,不添加修飾符

類的可見性:

  1. 對所有類可見 -- public
  2. 僅對本包可見 -- 默認,無修飾符。
  3. 對於內部類而言,本包和子類可見 -- protected
  4. 對於內部類而言,本類可見 -- private

JAVA核心技術筆記總結--第5章 繼承