【Java】java繼承的理解和應用

重要聲明:本文章僅僅代表了作者個人對此觀點的理解和表述。讀者請查閱時持自己的意見進行討論。

本文更新不及時,請至原文地址查看:【Java】java繼承的理解和應用

繼承這一特性在Java中的應用十分廣泛,幾乎任意一個項目甚至demo代碼片段裏,也可以見到java繼承的身影。因此有關繼承相關的知識點也必然是學習java編程必須要掌握的。在瞭解繼承之前,你需要對 java中的類 有一定的認識,否則可能本文閱讀相對困難。

一、繼承的作用

學習一個新事物前,往往都會有一個疑問。這是一個什麼樣的東西以至於我們要去學習它。它又能爲我們帶來什麼樣的便利呢?

在大多數的資料裏,幾乎都把java中的繼承比喻爲現實世界裏,你和你父母的關係那樣。不得不說的的卻卻沒有問題。但是似乎總有一點差強人意。假如你父親是一位成功的商人,可你不一定是;假如你母親是一位著名的畫家,你也不一定是畫家!在別人的眼裏,你頂多是成功商人的兒子、是著名畫家的女兒。但你不是他們,你也不一定能做到他們那樣。

而在程序裏,不一樣。程序裏所說的繼承,是指“類”之間的繼承。而現實裏,並不存在“類”相關的繼承的說話(不槓應該就沒有吧[滑稽保命])。你父親是一個對象,你是一個對象,你們都是“人類”這個類的實例。這也許就是程序裏的繼承與現實中的繼承的區別了。

程序中的類,通常是指:對一種有相共通的屬性和行爲功能的實例進行一個抽象描述。描述方式則通過class來定義。當你定義好了之後,便可通過這個定義的規則來構造具有相同或相似功能的不同對象了。

程序是人寫的,不可能一開始就明白一個程序將來會遇到什麼樣的功能和需求。因此程序是不斷去完善的。有一天突然要求添加一個功能,但此功能的絕大部分和已有功能模塊相同,僅僅只有一部分擴展的新功能。這時候,如果之前程序設計得當,便可以十分方便的通過繼承來實現新功能需求了。

簡單來說,java程序裏的繼承是“類”與“類”進行繼承。對象與對象之間不存在繼不繼承的關係。子類可以使用父類提供的非私有方法。

“父類”又有人叫:基類、超類。

二、使用繼承

要最終明白參透繼承,使用動物阿貓、阿狗,人類老師、學生,這類具體事務進行類比。會把人帶入死衚衕,會感覺: 合着我在這兒繼來繼去,就只是print了一堆name和age?到底繼承幹了啥、帶來了啥。沒感覺!

1、創建第一個類

所以我將通過具體功能需求來演示使用繼承爲我們開發帶來的便利。請看下面定義的一個類:

// Rectangle.java
public class Rectangle {
    public float sideLength1;
    public float sideLength2;

    public Rectangle (float sideLength1, float sideLength2){
        this.sideLength1 = sideLength1;
        this.sideLength2 = sideLength2;
    }

    public float calcArea(){
        return sideLength1 * sideLength2;
    }
}

很明顯,這是一個矩形類,sideLength 表示了矩形的邊長,矩形有四邊,但對邊相等,因此只需要定義兩邊的變量來保存數據就好了。構造函數要求了必須傳入兩個邊長來創建對象。同時,提供了一個方法 calcArea 計算這個矩形的面積,並返回結果。這個類十分簡單,相信每一個有心學習java的人都能夠看懂。

不如先來看看如果使用它,實現面積的計算:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle rect1 = new Rectangle(12.24f, 3.15f);
        float rect1Area = rect1.calcArea();
        System.out.println("矩形的面積是:" + rect1Area);
    }
}

構造矩形,計算面積,輸出結果。很是easy。那麼現在新需求來了。請開發一個類,實現立方體的體積計算。

2、繼承第一個類

要實現立方體體積計算,數學公式:體積=底面積×高。而我們第一個類的矩形面積計算已經是實現好了,有現成的了,那我們的體積計算還需要再寫一遍面積計算的代碼嗎?既然我們有這樣的實現,何不直接利用呢?繼承的有用之處就體現出來了,開始繼承實現:

// Cuboid.java
public class Cuboid extends Rectangle {
    public float sideLength3;

    public Cuboid (float sideLength1, float sideLength2, float sideLength3) {
        super(sideLength1, sideLength2);
        this.sideLength3 = sideLength3;
    }

    public float calcVolume() {
        return super.calcArea() * sideLength3;
    }
}

由於立方體存在第三條邊長,因此在矩形的基礎上,只需要再多定義一個參數用來保存第三條邊長的值就好了。提示提供了一個 calcVolume 方法用於計算體積。可以看到,在方法內部,直接使用了父類(矩形類)實現好的計算面積方法,然後再乘以第三條邊長,就計算出了體積。

立方體(Cuboid)類的構造函數注意

可以看到,子類構造函數裏面使用了一句:super(int,int)。它的作用就是將調用父類的構造函數,將父類的值得到初始化。爲什麼要這樣做?要弄明白這件事,可以由另一件事推導出來。首先,你肯定知道並對這句話沒有疑問:“子類繼承了父類,那麼子類就可以使用父類裏面所有的非私有方法”。既然能使用,那麼肯定父類就離不開正常初始化這樣一個操作流程。而這裏我們的 Rectangle 類有且只有一個構造函數,子類繼承它,要想 Rectangle 正常初始化,並且子類能夠順利使用得到希望的結果,那就必須要在構造子類時先將父類初始化完畢,這也是爲什麼必須保證調用父類構造函數的位置必須是構造函數第一行。因爲你有可能第二行就要使用父類裏的方法,不調用父類的構造函數又如何正常的使用呢。

另外,父類如果存在沒有參數的構造函數(或者沒有寫構造函數),那麼子類就可以不需要在代碼裏調用父類的構造函數(但不代表系統不會調用)。在初始化子類的過程中,也依然會先調用父類的構造函數去初始化父類。你要明白,父類也是人寫的,根據這些特性,寫父類的人知道自己的類如何使用才能正常,他會明確指定構造函數有沒有參數的。當使用者使用父類時,不必與原作者溝通即可知道父類的使用方式。也正是因爲這一套規矩的作用。

現在,不妨試試,使用 Cuboid 類來進行一次體積計算:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Cuboid cubo1 = new Cuboid(12.24f, 3.15f, 4.3f);
        float cubo1Volu = cubo1.calcVolume();
        System.out.println("立方體的體積是:" + cubo1Volu);
    }
}

這就是繼承。使用現有實現了某功能的類作爲父類,在此基礎上再實現自己需要的功能。達到重用、擴展功能的效果。

3、重寫

重寫:是指子類和父類有相同名字,相同參數個數的方法,我們就說子類的這個方法重寫了父類的對應方法。上文我們創建了 Rectangle 類,他可以計算矩形面積。現在我們希望它計算正方形的面積,雖然目前來看,它可以實現。但人們對於正方形的感知是隻需要一個參數,而 Rectangle 需要兩個參數。因此,我們不妨將其簡化一下:

// Square.java
public class Square extends Rectangle {
    public Square(float size) {
        super(size, size);
    }

    @Overide
    public float calcArea() {
        return Math.pow(super.sideLength1, 2);
    }
}

@Overide 註解是可選項,它的作用標記了此方法重寫了父類的 calcArea 方法,其作用更多是告訴閱讀代碼的人,這個方法重寫了父類的方法。由於我們都知道正方形只需要提供一個長度即可,因此 Square 構造函數只需要傳入一個參數。而父類 Rectangle 構造函數明確要求傳遞兩個參數,既然正方形兩邊相同長,因此可以直接將 size 同時傳遞給父類構造函數,這沒有什麼問題,並且是很常見的做法。

再往下看,可以看到子類裏也有一個和父類相同的 calcArea 方法,我們開發 Square 時,認爲父類計算面積的方式欠佳,希望使用自己的邏輯來計算正方形的面積。方法中使用了 Math.pow() 方法來計算面積,直接使用熟知的數學公式:正方形面積=邊長的平方。這樣當我們構造正方形時,再調用 calcArea () 方法,就不再使用父類的方法了,而是使用咱們重寫的方法了。

當然,你認爲父類計算面積的方法十分棒,而你又想在計算面積時額外新增一些其他功能,比如:輸出正方形的周長。那麼你可以這樣:

// Square#calcArea();
@Overide
public float calcArea() {
    float result = super.calcArea(); // 先使用父類實現好的方法得到面積。

    // 輸出正方形的周長。
    System.out.println("正方形的周長爲:" + (super.sideLength1 * 4));

    // 將面積結果返回。
    return result;
}

上述代碼,旨在告訴你,你可以重寫父類的方法,並且你還可以在你的方法裏面調用父類的這個方法來爲你達到前一步已達到的功能,然後再在此基礎上實現你更多的擴展功能。上方代碼的擴展功能就是在計算了面積的同時還打印了正方形的周長。周長和麪積始終是兩個不同的東西,同時出現計算面積方法裏顯得不妥。那麼我們可以進一步優化 Square 類,來完成周長和麪積的分別計算。

// Square.java
public class Square extends Rectangle {
    public Square(float size) {
        super(size, size);
    }

    @Overide
    public float calcArea() {
        return Math.pow(super.sideLength1, 2);
    }

    public float roundLength() {
        return super.sideLength1 * 4;
    }
}

新增了一個方法 roundLength ,用它來計算正方形的周長。這是隻有 Square 正方形類纔有的方法。父類沒有這個方法。 不如先來測試一下:

// Demo.java
public class Demo {
    public static void main(String []args) {
        Square squ1 = new Square(7.24f);
        float squ1RoundLength = squ1.roundLength();
        System.out.println("正方形的面積是:" + squ1RoundLength);
    }
}

運行程序,正常輸出結果。下面我們再來看看另一個在繼承中值得關注的問題“引用”問題。

三、父類引用子類

“父類引用子類”。其實不應該這樣命名,這是極其不規範、甚至不是很正確的。但它或許又表達了這一個知識點的大意。在程序裏,我們經常使用父類的應用去保存一個具體子類的實例。實際上,只要類A 是B、C、D、E類的父類甚至好幾級之上的父類,那麼這些所有子類的實例對象都可以通過A類創建一個引用字段去保存。比如下面這樣:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle shape1 = new Square();
        Rectangle shape2 = new Cuboid();
    }
}

這是沒有任何問題的。上述代碼中, 等號左邊,我們可以描述爲:父類創建的引用。等號右邊,具體子類實例化對象。通過等號,將實例對象使用父類進行引用。這和所有引用類似,把對象的內存地址保存在了 shape1、shape2 這兩個字段裏面。它們永遠都是new的時候對應的類的對象。至於爲什麼能把子類的引用賦值給父類創建的引用字段呢?

你需要思考。這件事也和“子類可以使用父類所有非私有的方法”有關係。且看我給你一個描述:子類繼承了父類,那麼子類就必定擁有了父類擁有的方法,所以父類引用保存了子類實例時,能夠使用的方法只能是父類裏提供的方法,而這個子類實例的對象又必然擁有這些方法。無論如何,都可以正確調用而不會出現找不到的問題。

反過來則不可以

很顯然,子類可以自己新增很多自己的方法,父類並沒有這些方法,如果反過來引用,則可以調起子類方法,但父類對象並沒有這些方法。不行不行。

關於強制轉換

對於基本變量,int a = (int)2.3f。這樣的強制轉換你應該非常熟悉了。現在,對象也可以強制轉換了。實際上,只是做了一個引用類型轉換。具體實例還是那個不變的實例。就像下面這樣:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle shape1 = new Square();
        Square square = (Square)shape1;
    }
}

你看到上述代碼時可能會表示很白癡。因爲你看到了實例創建的時候本來就是一個 Square ,最後你又強制轉換成 Square,意義何在?先撇開它的意義,先梳理一下,首先建立一個對象並將其賦值給 Rectangle 父類引用。當你使用父類引用時,在代碼裏只能顯示的調用父類裏的方法,當你希望調用 Square 自己的方法時,就辦不到了。不過幸好,你有強制轉換。轉換了啥?其實就只將引用類型進行了轉化,從父類引用轉化爲了子類引用。

注意了,這樣的強制轉換隻能在你知道它具體是哪一個子類時才能進行。否則強制轉換將報錯的。通過 instanceof 語句,可以判斷某個對象是否是某個類的實例:

if (shape1 instanceof Square) {
    System.out.println("shape1是一個正方形。");
}

四、Object

在java裏,Object類是所有類的父類,意味着,不論什麼對象,你都可以使用 Object 來創建引用進行傳遞。

五、其它項

要注意,繼承只能繼承一個父類,不可以繼承多個父類。如果你的類擔心別人繼承。你可以將類定義爲final的:

// Test.java
public final class Test {
    // 使用 final 修飾的類,是不允許被繼承的。
}

有時候,你希望別人繼承你的類,但你不希望其中某一個方法被重寫。你也可以使用final修飾:

// Test.java
public class Test {

    // 使用final修飾的方法不能被重寫。
    public final void function1 () {
        // todo...
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章