繼承和多態


  面向對象編程允許從已經存在的類中定義新類,這稱為繼承。繼承是面向對象編程的一個重要特征。假設在QQ寵物游戲中要定義一個類,對狗狗,企鵝還有豬豬建模。這些類有很多共同的特性。我們可以使用繼承來編碼冗余並使系統更易於理解和易於維護。

父類和子類

  使用類來對同一類型的對象建模。不同的類也可能會有一些共同的特征和行為,這些共同的特征和行為都統一放在一個類中,它是可以被其他類所共享的。可以定義特定的類繼承自通用類。這些特定的類繼承通用類中的特征和方法。考慮一下QQ寵物對象。假設要設計類建模像Dog和Penguin這樣的寵物對象。寵物對象有許多共同的行為和屬性。這樣一個通用類Pet可以用來建模所有的寵物對象。

package edu.uestc.avatar.inherit.pet;

/**
 * 寵物這一類事務共同的屬性和行為抽取在一個通用的公共類-----Pet
 * @author Tiger
 *
 */
public class Pet {
    //共同的特征---名字
    private String name;
    //共同的特征---健康值
    private int health;
    //共同的特征---愛心值
    private int love;
    
    public Pet(String name, int health, int love) {
        System.out.println("pet---構造方法");
        this.name = name;
        this.health = health;
        this.love = love;
    }

    /**
     * 自白
     */
    public void sayHello() {
        System.out.print("我的名字叫" + this.name + ",我的健康值是" + health + ",我和主人的親密度是" + love);
    }
    
    /**
     * 吃東西
     */
    public void eat() {
        if(this.health <= 95) {
            this.health += 3;
            this.love -= 5;
        }
    }
    
    /**
     * 陪主人玩
     */
    public void play() {
        if(this.health > 60) {
            this.health -= 5;
            this.love += 3;
        }
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getHealth() {
        return health;
    }
    public void setHealth(int health) {
        this.health = health;
    }
    public int getLove() {
        return love;
    }
    public void setLove(int love) {
        this.love = love;
    }
}

  Dog類繼承了Pet類所有可以訪問的數據域和方法。除此之外,它還有個新的數據域strain以及與strain相關的setter和getter方法。

package edu.uestc.avatar.inherit.pet;

/**
 * Dog擴展自通用的寵物類Pet(繼承)
 *
 */
public class Dog extends Pet{
    private String strain;
    //在子類中通過super()調用父類的構造方法
    public Dog(String name,int health,int love,String strain) {
        super(name,health,love);//調用父類的構造方法必須在第一句
        this.strain = strain;
        System.out.println("dog---構造方法");
    }
    /**
     * super關鍵字
     *         1、子類調用父類中的方法
     *         2、在子類構造方法中調用父類的構造方法
     */
    public void sayHello() {
        super.sayHello();//子類調用父類中的方法
        System.out.println(",我的類型是" + strain);
    }
    
    public String getStrain() {
        return strain;
    }

    public void setStrain(String strain) {
        this.strain = strain;
    }
    
}

  Penguin類繼承了Pet類所有可以訪問的數據域和方法。除此之外,它還有個新的數據域gender以及與gender相關的setter和getter方法

package edu.uestc.avatar.inherit.pet;

/**
 * 子類是父類的特殊化,每個子類的實例都是父類的實例。
 *
 */
public class Penguin extends Pet{
    private String gender;
    
    public Penguin(String name,String gender) {
        super(name,100,100);
        this.gender = gender;
        System.out.println("penguin---構造方法");
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
    
    /**
     * 為滿足子類的需要,子類需要覆蓋父類中定義的方法實現----------方法重寫
     * 1、訪問修飾符:子類重寫方法訪問權限不能小於父類的訪問修飾權限
     * 2、返回值類型:一般具有相同的返回值類型,子類重寫的方法返回值類型不能大於父類的返回值類型
     * 3、異常類型:子類重寫的方法不能拋出比父類更大類型的異常
     * 4、僅當實例方法可訪問時,才能進行方法重寫
     * 5、靜態方法也能被繼承,靜態方法不能被覆蓋。
     * @Override 編譯器可幫我們檢查是否是符合方法重寫要求的
     */
    @Override
    public void sayHello() {
        System.out.println("我的名字叫" + this.getName() + ",我的健康值是" + getHealth() + ",我和主人的親密度是" + getLove() + ",我的性別是:" + gender);
    }
//企鵝所特有的方法:撒嬌
   public void 撒嬌(){

} }

  在java術語中,如果Dog類繼承自Pet類,那么就將Dog類稱為次類,將Pet稱為超類。超類也稱為父類或基類;次類也稱為子類,擴展類或者派生類

  關於繼承應該注意的幾個關鍵點

  • 和傳統的理解不同,子類並不是父類的一個子集。實際上,一個子類比它的父類包含更多的信息和方法。
  • 父類中的私有屬性在該類之外是不可訪問的。因此,不能在子類中直接使用。但是,如果父類中定義了公共的setter/getter,那么可以通過這些公共的setter/getter來修改和訪問它們
  • 繼承是用來為is-a關系建模的。不要僅僅為了重用方法而盲目擴展一個類。例如:盡管Person類和Tree類可以共享類似高度和重量這樣的通用特性,但是從Person類擴展出Tree類是毫無意義的。一個父類和它的子類之間必須存在“是一種”(is-a)關系
  • 不是所有的is-a關系都該用繼承來建模。例如:正方形是一種矩形,但是不應該定義一個Square類來擴展Rectangle類,因為width和height屬性並不適合於正方形。
  • java是不允許多重繼承的。一個java類只能直接繼承自一個父類。多重繼承可以通過接口來實現。

  使用super關鍵字

  關鍵字super指代父類,可以用於調用父類中的普通方法和構造方法,前面介紹了關鍵字this的作用,它是對調用對象的引用。關鍵字super是指這個super關鍵字所在的類的父類。關鍵字super可以用於兩種途徑:

1)調用父類構造方法

  構造方法用於構建一個類的實例。不同於屬性和普通方法,父類的構造方法不會被子類繼承。它們只能使用super從子類的構造方法中調用。語法:super或者super(parameters)

public  Penguin(String name,String gender) {
    //顯式的調用父類的構造方法
    super(name);//必須是構造方法的第一題語句
    this.gender = gender;
    System.out.println("我是Penguin的構造方法");
}

 構造方法鏈

  在任何情況下,構造一個類的實例時,將會調用沿着繼承鏈的所有父類的構造方法。當構造一個子類對象時,子類構造方法會在完成自己任務之前,首先調用它的父類構造方法。如果父類繼承自其他類,那么父類構造方法又會在完成自己任務前調用它自己的父類的構造方法。這個過程持續到沿着這個繼承體系結構的最后一個構造方法被調用為止。如果沒有被顯式的調用,編譯器將會自動添加super()作為構造方法的第一條語句。

2)調用父類方法

方法重寫

  出現的前提:有子類繼承父類

       出現的場景:當子類繼承了父類的方法后發現父類的方法不適用於子類,則子類可以通過重寫父類的方法。

       在調用時,調用的是子類重寫后的方法,而不是來自於父類的方法。

       重寫的規則:

              1)要求子類與父類的方法:返回值類型、方法名、形參列表必須完全相同。

              2)子類的修飾符權限不能小於父類的修飾符權限

              3)若父類方法拋異常,那么子類方法拋的異常類型不能大於父類方法拋的異常類型

              4)子類可以重寫父類的靜態方法,但是必須是同為靜態的(對父類靜態方法來說是隱藏,可以使用父類.靜態方法的形式訪問)

Object類及其toString()方法

  java中的所有類都繼承自java.lang.Object類。toString()方法:

   1)當我們打印一個引用數據類型的對象時,默認調用的是這個對象的toString();//Loan@15037e5

        2)當我們重寫了toString()后再打印該對象,會調用重寫后的toString();

        3)像String、File、Date等類已經重寫了toString方法。

多態

  多態意味着父類的變量可以指向子類對象。理解成一種事物的多種表現形態.

  首先,定義兩個有用的術語:子類型和父類型。一個類實際上定義了一種類型。子類定義的類型稱為子類型(subtype),父類定義的類型稱為父類型(supertype)。因此,可以說Dog時Pet的子類型,而Pet是Dog的父類型。

  繼承關系使一個子類繼承父類的特征,並且附加一些新特征。子類是它的父類的特殊化,每個子類的實例都是其父類的實例,但是反過來就不成立。例如:每個狗狗都是一個寵物對象,但並非每個寵物對象都是狗狗。因此,總可以將子類的實例傳給需要父類型的參數。 

/*
 *  多態
 *  動態綁定:編譯看左邊(父類型),運行看右邊
 *  Pet[]:聲明類型
 *  dog,penguin:實際類型
 */
Pet[] pets = {new Dog("毛毛",100,100,"斑點狗"),new Penguin("pedro", "male"),new Dog("天天",100,100,"二哈")};
   for(Pet pet : pets)
      pet.sayHello();
}

動態綁定

  在程序運行過程中,根據具體的實例對象才能具體確定是哪個方法。

  動態綁定是多態性得以實現的重要因素,它通過方法表來實現:每個類被加載到虛擬機時,在方法區保存元數據,其中,包括一個叫做方法表(methodtable)的東西,表中記錄了這個類定義的方法的指針,每個表項指向一個具體的方法代碼。如果這個類重寫了父類中的某個方法,則對應表項指向新的代碼實現處。從父類繼承來的方法位於子類定義的方法的前面。

  聲明類型和實際類型 

//一個變量必須被聲明為某種類型 Pet pet聲明類型
Pet pet = new Dog("毛毛",100,100,"斑點狗");//new Dog("毛毛",100,100,"斑點狗");實際類型

  pet調用哪個play()方法由pet的實際類型決定。這稱為動態綁定。

  我們知道,向上轉型時,用父類引用執行子類對象,並可以用父類引用調用子類中重寫了的同名方法。但是不能調用子類中新增的方法。
 
  在代碼的編譯階段,編譯器通過聲明對象的類型(即引用本身的類型)在方法區中該類型的方法表中查找匹配的方法(最佳匹配法:參數類型最接近的被調用),如果有則編譯通過。(這里是根據聲明的對象類型來查找的,所以此處是查找Father類的方法表,而Father類方法表中是沒有子類新增的方法的,所以不能調用。)編譯階段是確保方法的存在性,保證程序能順利、安全運行。  
 
  pet.play()調用的是Dog中的play(),這里就是動態綁定機制的真正體現。
 
  編譯階段在聲明對象類型的方法表中查找方法,只是為了安全地通過編譯(也為了檢驗方法是否是存在的)。而在實際運行這條語句時,在執行 Pet pet = new Dog("毛毛",100,100,"斑點狗");這一句時創建了一個Dog實例對象,然后在pet.play()調用方法時,JVM會把剛才的pet對象壓入操作數棧,用它來進行調用。而用實例對象進行方法調用的過程就是動態綁定:根據實例對象所屬的類型去查找它的方法表,找到匹配的方法進行調用。我們知道,子類中如果重寫了父類的方法,則方法表中同名表項會指向子類的方法代碼;若無重寫,則按照父類中的方法表順序保存在子類方法表中。故此:動態綁定根據對象的類型的方法表查找方法是一定會匹配(因為編譯時在父類方法表中以及查找並匹配成功了,說明方法是存在的。這也解釋了為何向上轉型時父類引用不能調用子類新增的方法:在父類方法表中必須先對這個方法的存在性進行檢驗,如果在運行時才檢驗就容易出危險——可能子類中也沒有這個方法)。  

  動態綁定和靜態綁定

  程序在JVM運行過程中,會把類的類型信息、static屬性和方法、final常量等元數據加載到方法區,這些在類被加載時就已經知道,不需對象的創建就能訪問的,就是靜態綁定的內容;需要等對象創建出來,使用時根據堆中的實例對象的類型才進行取用的就是動態綁定的內容。  

  編譯器在每次調用方法時都要進行搜索,時間開銷相當大。因此虛擬機會預先為每個類創建一個方發表(method table),其中列出了所有方法的簽名和實際調用的方法。  

對象轉換和instanceof運算符

  將一個類型強制轉換為另外一個類型的過程稱為類型轉換,語法格式為:

Pet pet = new Penguin();
Penguin penguin = (Penguin)penguin 

  在java中,每個對象變量都屬於一個類型。將一個值存入變量時,編譯器將檢查是否允許該操作。將一個子類的引用賦給超類類型(向上轉型),編譯器是允許的。但將一個超類的引用賦給一個子類變量(向下轉型),必須進行類型轉換。

  如果視圖在繼承鏈上進行向下的類型轉換,並且謊報有關對象包含的內容,會發生什么情況呢?

Pet pet = new Penguin();
Dog dog = (Dog)pet;

  java運行時系統將報告這個錯誤,產生一個ClassCastException異常。如果沒有捕獲這個異常,程序就會終止。因此,應該養成一個良好的程序設計習慣,在進行類型轉換之前,先查看一下是否能夠成功地轉換。使用instanceof運算符就可以實現

if(pet instanceof Penguin){
   Penguin penguin = (Penguin)pet;
   ...
}

//jdk 14的模式匹配寫法
if(pet instanceof Penguin penguin){
  ...
}

  實際上,通過類型轉換調整對象的類型並不是一個好的做法。在列舉的示例中,大多數情況並不需要將Pet對象轉成Penguin對象。兩個類的對象都能夠調用play方法,這是因為實現多態性的動態綁定機制能夠自動的找到相應的方法。只有在使用Penguin中特有的方法時才需要進行類型轉換。

Object類的equals方法

   1)equals不適用於基本數據類型,equals只適用於引用數據類型

        2)如果不重寫equals方法,則比較的是兩個對象的地址值,等效於“==”.因此,應該根據需要在自己的客戶類中重寫equals方法。

        3)像String、File、Date等類重寫了Object的equals方法,比較的是兩個String對象的具體的值,而不是地址值。

protected數據和方法

  1、基類的protected成員是包內可見的,並且對子類可見;

  2、若子類與基類不在同一包中,那么在子類中,子類實例可以訪問其從基類繼承而來的protected方法,而不能訪問基類實例的protected方法

防止擴展和重寫

  final關鍵字:最終的,可以用來修飾類、屬性、方法

  1.final修飾類:這個類不能被繼承

  2.final修飾方法:這個方法不能被重寫,當一個方法所要體現的功能已經被確定,則用final修飾。

  3.final修飾屬性:表示常量,則該常量必須有初始化值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM