天天看點

Java學習-繼承、super關鍵字

1、引例

2、繼承

2.1 概述

2.2優缺點

2.3Java繼承的類型

3、關鍵字

3.1super關鍵字

3.2super與this關鍵字的差別

4、繼承需要注意的幾個問題

4.1成員變量和方法

4.2構造器

4.3繼承的執行順序問題

4.4繼承破壞父類封裝性問題

4.5何時适合用繼承

1、引例

假如現在有四個類它們的成員屬性分别為:

  • 學生類:姓名,性别,年齡,學校
  • 教師類:姓名,性别,年齡,學科
  • 勞工類:姓名,性别,年齡,工種
  • 農民類:姓名,性别,年齡,收入

      對于上面的四個類我們在實際寫代碼的時候會發現,發我在重複性地寫一些代碼比如(姓名、性别、年齡)是這四個類共有的成員屬性。這時候我們就可以建一個person類把(姓名,性别,年齡)作為它的成員屬性,而上面四個類通過繼承這個person類實作對共有的成員屬性的調用以提高代碼的複用性

Java學習-繼承、super關鍵字

2、繼承

2.1 概述

     繼承是java面向對象程式設計技術的一塊基石,因為它允許建立分等級層次的類。繼承就是子類繼承父類的特征和行為,使得子類對象(執行個體)具有父類的執行個體域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

  • 繼承是面向對象思想的三大特性之一,使類與類之間産生

    特殊

    的關系,即

    is-a

    關系。
  • 繼承是

    從已有類中派生出新的類

    ,新的類能

    吸收已有類的屬性和方法

    ,并且能

    拓展新的屬性和行為

  • 在Java中使用

    extends

    關鍵字表示繼承,文法表示為: 

    class 子類 extends 父類{}

  • 子類被稱為

    派生類

    ,父類又被稱為

    超類

  • 子類繼承父類,表示

    子類是一種特殊的父類

    ,子類擁有父類的非private屬性和方法,并且子類可以拓展具有父類所沒有的一些屬性和方法。
  • 子類即使不擴充父類,也能維持擁有父類的操作。

2.2優缺點

繼承的好處是:

  • 提高了代碼的複用性
  • 提高了代碼的維護性
  • 讓類與類之間産生了關系,是多态的前提

繼承的缺點是:

  • 增加了耦合性(OOP思想開發原則:高内聚,低耦合    耦合:類與類之間的關系  内聚:自身完成事情的能力)

2.3Java繼承的類型

  • Java隻支援單繼承,不支援多重繼承
Java學習-繼承、super關鍵字
Java學習-繼承、super關鍵字

多重繼承會存在安全隐患,因為當繼承的多個類都存在相同的屬性或方法體不同的方法,子類進行調用時,就會産生不知道該調用哪一個類中的方法的情況

  • Java支援繼承體系
Java學習-繼承、super關鍵字

3、關鍵字

3.1super關鍵字

我們可以通過super關鍵字來實作對父類成員的通路,用來引用目前對象的父類。

兩種方法:

 1、用在子類的構造方法裡(初始化用),主要是調用父類的預設構造方法,如果父類有不止一個構造方法,可以通過super指定具體的構造函數,比如 super(paras);

注意:super表示目前類的父類,super()調用的是父類預設的構造方法,即這樣可以對父類進行初始化。如何沒有對父類進行初始化,當子類調用父類的方法時,便會從邏輯上出現錯誤,因為沒對父類初始化,父類的方法和屬性便沒有記憶體空間。

 2、用在子類裡調用隐藏或重寫的屬性或行為,比如 super.onDestroy()等等

3.2super與this關鍵字的差別

1. super(參數):調用基類中的某一個構造函數(應該為構造函數中的第一條語句)

2. this(參數):調用本類中另一種形成的構造函數(應該為構造函數中的第一條語句)

3. super: 它引用目前對象的直接父類中的成員(用來通路直接父類中被隐藏的父類中成員資料或函數,基類與派生類中有相同成員定義時如:super.變量名 super.成員函資料名(實參)

4. this:它代表目前對象名(在程式中易産生二義性之處,應使用this來指明目前對象;如果函數的形參與類中的成員資料同名這時需用this來指明成員變量名)

5. 調用super()必須寫在子類構造方法的第一行,否則編譯不通過。每個子類構造方法的第一條語句,都是隐含地調用super(),如果父類沒有這種形式的構造函數,那麼在編譯的時候就會報錯。

6. super()和this()類似,差別是,super()從子類中調用父類的構造方法,this()在同一類内調用其它方法。

7. super()和this()均需放在構造方法内第一行。

8. 盡管可以用this調用一個構造器,但卻不能調用兩個。

9. this和super不能同時出現在一個構造函數裡面,因為this必然會調用其它的構造函數,其它的構造函數必然也會有super語句的存在,是以在同一個構造函數裡面有相同的語句,就失去了語句的意義,編譯器也不會通過。

10. this()和super()都指的是對象,是以,均不可以在static環境中使用。包括:static變量,static方法,static語句塊。

11. 從本質上講,this是一個指向本對象的指針, 然而super是一個Java關鍵字。

4、繼承需要注意的幾個問題

4.1成員變量和方法

  1. 子類隻能繼承父類的所有非私有的成員變量和方法。可以繼承public protected 修飾的成員,不可以繼承private修飾的。
  2. 但是子類可以通過父類中提供的public 的setter和getter方法進行間接的通路和操作private 的屬性
  3. 對于子類可以繼承父類中的成員變量和成員方法,如果子類中出現了和父類同名的成員變量和成員方法時,父類的成員變量會被隐藏,父類的成員方法會被覆寫。需要使用父類的成員變量和方法時,就需要使用super關鍵字來進行引用。           (隐藏是針對成員變量和靜态方法,覆寫是針對普通方法。)
  4. 當建立一個子類對象時,不僅會為該類的執行個體變量配置設定記憶體,也會為它從父類繼承得到的所有執行個體變量配置設定記憶體,即使子類定義了與父類中同名的執行個體變量,也依然會為父類中定義的、被隐藏的變量配置設定記憶體。
  5. 如果子類中的執行個體變量被私有了 ,其父類中的同名執行個體變量沒有被私有,那麼子類對象就無法直接調用該變量,但可以通過先将對象變量強制向上轉型為父類型,在通過該對象引用變量來通路那個執行個體變量,就會得到的是父類中的那個執行個體變量。                                                                                                                                                                                          (補充:什麼是向上轉型和向下轉型?)

4.2構造器

  1. 子類不能繼承獲得父類的構造方法,但是可以通過super關鍵字來通路父類構造方法。
  2. 在一個構造器中調用另一個重載構造器使用this調用完成,在子類構造器中調用父類構造器使用super調用來完成。
  3. super 和 this 的調用都必須是在第一句,否則會産生編譯錯誤,this和super隻能存在一個。不能進行遞歸構造器調用,即多個構造器之間互相循環調用。
  4. 如果父類有無參構造時,所有構造方法(包含任意有參構造)自動預設都會通路父類中的空參構造方法。(自帶super();)
  5. 因為繼承的目的是子類擷取和使用父類的屬性和行為,是以子類初始化之前,一定要先完成父類資料的初始化。
  6. 在Java中,每個類都會預設繼承Object超類,是以每一個構造方法的第一條預設語句都是super()
  7. 如果父類沒有無參構造,反而有其他的有參構造方法時,子類繼承父類後,子類必須顯式的建立構造器,不論子類的構造器是否和父類構造器中參數類型是否一緻,都必須在子類的構造器中顯式的通過super關鍵字調用和父類構造器相應參數的構造方法,否則編譯都通不過。

代碼示例:

class Person {
 public Person(int age){
     System.out.println(age);
 }

}
class Student extends Person{
 public Student(int age) {
     super(age);
 }
  public Student(){
     super(10); //必須調用父類的有參構造
     System.out.println("子類可以建立其他類型構造器,但是必須顯式的用super調用父類構造器")
 }
}
           

也可以使用

this

先調用子類中的構造方法,再間接調用父類中的有參構造方法:

public class ExtendTest1  {
 public static void main(String[] args) {
     new Student();
 }

}
class Person {
 public Person(int age){
     System.out.println("父類有參構造");
 }

}
class Student extends Person{
 public Student(int age) {
     super(age);
     System.out.println("子類有參構造");
 }
 public Student(){
     this(10); //可以使用this先調用子類中的有參構造,進而間接調用父類中的有參構造
     System.out.println("子類無參構造");
 }
}
           

使用this,執行順序結果為:先調用了子類中無參構造,此無參構造會接着調用子類中的有參構造,又接着調用父類中的有參構造,此時首先執行完畢了父類有參構造,接着子類有參構造執行完畢,最後子類無參構造才執行完畢。

父類有參構造
子類有參構造
子類無參構造
           

以下這種是錯誤的:(因為當父類中沒有無參構造器時,父類中沒有這種類型的構造方法):

class Student extends Person{
 public Student(String name){
     super();
 } //錯誤的,因為當父類中沒有無參構造器時,父類中沒有這種類型的構造方法
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}
           

以下這種正确:(因為當父類中沒有無參構造器時,子類中的構造方法的類型在父類中有)

class Student extends Person{
 //因為當父類中沒有無參構造器時,子類中的構造方法的類型在父類中有
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}
           
class Student extends Person{
//因為當父類中沒有無參構造器時,子類中的構造方法的類型在父類中有
 public Student(String name ,int age){
     super(name,age);
 }
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}
           

結論:當父類中沒有無參構造器時,子類繼承父類,子類中的構造器方法類型可以和父類中的構造器不同,但是必須每個構造器都顯式的使用super關鍵字調用父類中的某個有參構造器,也可以使用this調用子類中的某個有參構造器,但這個有參構造器必須通過super通路父類中的有參構造器。

4.3繼承的執行順序問題

1、繼承體系中的構造器執行順序:

  • 當調用子類構造器執行個體化子類對象時,父類構造器總是在子類構造器之前執行。
  • 建立任何對象總是從該類所在繼承樹最頂層類的構造器開始執行,然後依次向下執行,最後才執行本類的構造器。如果父類通過this調用了同類中的重載構造器,就會依次執行此父類的多個構造器。

2、繼承體系中的

靜态域

執行順序:

  • 當調用子類構造器執行個體化子類對象時,父類優先于子類進行加載到記憶體,是以會先執行父類中的靜态域
  • 從該類所在繼承樹最頂層類開始加載,并執行其靜态域,依次向下執行,最後執行本類。
  • 靜态域優先于main方法,優先于構造器執行

3、父類和子類中

都有靜态代碼塊和構造代碼塊

class Test2_Extends {
  static {
    System.out.println("主類靜态塊");
}
public static void main(String[] args) {
    Zi z = new Zi();
}
}
class Fu {
static {
    System.out.println("靜态代碼塊Fu");
}

{
    System.out.println("構造代碼塊Fu");
}

public Fu() {
    System.out.println("構造方法Fu");
}
}

class Zi extends Fu {
static {
    System.out.println("靜态代碼塊Zi");
}

{
    System.out.println("構造代碼塊Zi");
}

public Zi() {
    System.out.println("構造方法Zi");
}
}

           

執行結果:

主類靜态塊
靜态代碼塊Fu
靜态代碼塊Zi
構造代碼塊Fu
構造方法Fu
構造代碼塊Zi
構造方法Zi
           

執行順序分析:

  1. 主類Test2_Extends先加載到記憶體,靜态域優先于main方法執行,先輸出了主類靜态塊,其中的main方法入棧執行,main方法中建立了子類對象
  2. 子類對象建立過程中,父類和子類都加載到記憶體中,并且Fu.class優先于Zi.class加載,父類中的靜态域先執行後,再執行子類中的靜态域,此時會第一個輸出:靜态代碼塊Fu,第二個輸出:靜态代碼塊Zi
  3. 建立對象時進入子類的構造器,因為Java是分層初始化的,是以會先初始化父類再初始化子類,子類構造器會自動預設先執行父類的構造器,因為構造代碼塊優先于構造方法執行,是以此時就會先執行父類的構造代碼塊後,再執行父類的構造方法。是以第三個輸出:構造代碼塊Fu,第四個輸出:構造方法Fu
  4. Fu類初始化結束後,子類初始化,第五個輸出的是:構造代碼塊Zi,第六個輸出:構造方法Zi

4.4繼承破壞父類封裝性問題

1、繼承嚴重破壞了父類的封裝性,每個類都應該它内部資訊和實作細節,而隻暴露必要的方法給其它類使用。但在繼承關系中,子類可以直接通路父類的成員變量(内部資訊)和方法, 進而造成子類和父類的嚴重耦合。

2、父類的實作細節對其子類不再透明,進而導緻子類可以惡意篡改父類的方法

改進方法:

  • 盡量隐藏父類的内部資料。
  • 盡量把

    父類的所有成員變量都設定成private通路類型

    ,不要讓子類直接通路父類的成員變量
  • 不要讓子類随意通路、修改父類的方法
  • 父類

    中那些僅為輔助其他的

    工具方法

    ,應該使用

    private

    修飾,讓子類無法通路方法;
  • 如果父類中的方法

    需要被外部類調用

    ,則必須以

    public

    修飾,但又

    不想讓子類重寫

    ,就可以使用

    final

    修飾符。
  • 如果

    希望父類的某個方法被子類重寫

    ,但

    不希望被其他類自由通路

    ,則可以使用

    protected

    來修飾方法。
  • 盡量不要在父類構造器中調用将要被子類重寫的方法。

檢視下面例子說明在父類構造器中調用被子類重寫的方法引發的錯誤:

package extend;

class Base
{
    public Base()
    {
        System.out.println("父類構造器");
        test();
    }
    public void test()           // ①号test()方法
    {
        System.out.println("将被子類重寫的方法");
    }
}
public class Sub extends Base
{
    public Sub(){
        System.out.println("子類構造器");
    }
    private String name="aa";
    public void test()         // ②号test()方法
    {
        System.out.println("子類test");
        System.out.println("子類重寫父類的方法,"
            + "其name字元串長度" + name.length());
    }
    public static void main(String[] args)
    {
        // 下面代碼會引發空指針異常
        Sub s = new Sub();
    }
}
           

執行結果:

父類構造器
子類test
Exception in thread "main" java.lang.NullPointerException
           

分析:

當建立Sub對象時,

先執行其父類構造器

,如果父類構造器調用了被子類重寫覆寫的方法,就

會調用被子類重寫後的②号test()方法

,子類的test方法

調用了子類的執行個體變量name

,父類直接調用的子類的test方法,

此時子類還未初始化,還未調用子類構造器,執行個體變量name還未被指定初始值,仍然為預設值null

,是以引發了空指針異常。

4.5何時适合用繼承

  • 子類需要額外增加屬性,而不僅僅是屬性值的改變。
  • 子類需要增加自己獨有的行為方式(包括增加新的方法或重寫父類的方法)。