Java的繼承到底是怎麼回事?看這篇讓你明明白白

語言: CN / TW / HK

theme: scrolls-light

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第25天,點選檢視活動詳情

一. 引言

在學習面向物件後,我們就可以使用類來描述物件共有的特徵(屬性)和行為舉止(方法),如果我們用類來描述貓、狗和企鵝,可以進行如下編碼:

``` public class Cat {

private String name;//名字

private int age;//年齡

private String strain; //品種
//省略getter和setter方法

}

public class Dog {

private String name;//名字

private int age;//年齡

private String strain; //品種
//省略getter和setter方法

}

public class Penguin{

private String name;//名字

private int age;//年齡

private String sex; //性別
//省略getter和setter方法

} ```

你會發現,在上面的3個類中存在大量的重複程式碼,那麼該如何優化呢?這裡我們可以使用繼承來優化

二. 繼承

1. 什麼是繼承

當我們編寫的多個類中存在相同的屬性或方法時,可以將這些類中相同的屬性和方法抽取到一個新的類中,然後再讓這些類繼承於這個新類,就可以重用新類中的屬性和方法,這些類稱為子類,這個新類稱為父類,這就是Java中的繼承。

2. 如何使用繼承

我們將上面的案例抽取出一個父類如下:

``` public class Animal {

String name;//名字

int age;//年齡

} ```

接下來只需要繼承於這個父類即可。在Java中,繼承使用extends關鍵字 來表示,上面的三個類修改如下:

``` public class Cat extends Animal{//貓類繼承於動物類

private String strain; //品種
 //省略getter和setter方法

}

public class Dog extends Animal{//狗類繼承於動物類

private String strain; //品種
 //省略getter和setter方法

}

public class Penguin extends Animal{//企鵝類繼承於動物類

private String sex; //性別
 //省略getter和setter方法

} ```

接下來我們可以編寫一個測試案例。

``` public class AnimalTest {

public static void main(String[] args) {
    Cat c = new Cat();
    c.setName("苗苗");
    c.setAge(1);
    c.setStrain("咖啡貓");
}

} ```

我們發現,在 Cat 類中並沒有 setNamesetAge 方法,但卻可以直接使用這些方法,說明這兩個方法都是從父類中繼承過來的,其他兩個類也一樣。

3. 子類能夠繼承父類的哪些屬性和方法

3.1 API文件

官方文件中有這樣的描述:

A subclass inherits all of the public and protected members of its parent, no matter what package the subclass is in. If the subclass is in the same package as its parent, it also inherits the package-private members of the parent.

解釋說明:

不論子類與父類是否在同一個包中,父類中使用 public 或者 protected 修飾的屬性和方法,都能夠被子類繼承。如果子類和父類在同一個包中,那麼子類還能繼承受包保護的屬性和方法(受包保護指的是沒有使用訪問修飾符的屬性和方法)。

A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.

解釋說明:

子類不會繼承父類中定義的私有成員。但如果父類有提供使用 public 或者 protected 修飾的訪問該欄位的方法,這些方法也能在子類中被使用。

為了驗證這些文件中的說法是否正確,接下來我們通過幾個案例來進行驗證。

3.2 案例一

``` public class Animal {

private String name;//修改訪問修飾符為private

int age;//年齡
//省略getter和setter方法

}

public class Cat extends Animal {

private String strain;

public String getStrain() {
    return strain;
}

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

public void show(){ //新增一個show方法,列印name和age屬性
    System.out.println(name + age);
}

} ```

此時編譯也出錯,name 屬性不能訪問,但 age 屬性可以。

該案例說明了父類中私有的屬性不能被繼承,但受包保護的屬性可以被繼承

3.3 案例二

``` package com.qf.oop; //給父類新增包名

public class Animal {

private String name;//修改訪問修飾符為private

int age;//年齡
//省略getter和setter方法

}

package com.qf.oop.sub; //給子類新增包名

import com.qf.oop.Animal;

public class Cat extends Animal {

private String strain;

public String getStrain() {
    return strain;
}

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

public void show(){
    System.out.println(name + age);
}

} ```

此時編譯也出錯,nameage 屬性都不能訪問。

該案例說明了在不同的包中,子類不能繼承父類中的私有屬性和受包保護的屬性。

3.4 案例三

``` package com.qf.oop;

public class Animal {

public String name; //修改訪問修飾符為public

protected int age; //修改訪問修飾符為protected

//省略getter和setter方法

}

package com.qf.oop.sub;

import com.qf.oop.Animal;

public class Cat extends Animal {

private String strain;

public String getStrain() {
    return strain;
}

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

public void show(){
    System.out.println(name + age);
}

} ```

此時編譯正常。

該案例說明了在不同的包中,子類可以繼承父類中公開的屬性和受保護的屬性 方法的繼承與屬性的繼承規則一致。

三. 方法重寫

1. 什麼是方法重寫

方法重寫指的是在具有繼承關係的子類中,如果存在一個成員方法,與父類中的成員方法有相同的簽名和返回值型別,那麼,這個方法就重寫了父類中的成員方法,稱為方法重寫。

方法簽名指的是方法名、引數型別和引數個數。

相同的方法簽名指的是方法名、引數型別、引數個數和引數出現的位置均相同。

2. 為什麼要使用方法重寫

我們知道方法表示的是行為舉止,子類繼承父類,當然也可以繼承父類的行為舉止。但我們經常會遇到子類和父類做一件事情的方式不一樣,也就是說,子類中的方法實現和父類中的方法實現存在差別。例如:動物會吃食物,但不同的動物吃的食物也不一樣。 

``` public class Animal{

public void eat(){
    System.out.println("動物吃食物");
}

}

public class Cat extends Animal{

public void eat(){ System.out.println("貓吃魚"); } } ```

子類 Cat 中的 eat 方法與父類 Animal 中的 eat 方法具有相同的簽名和返回值型別,這樣的方法我們稱其為重寫了父類中eat 方法。

為了方便檢視重寫的方法,Java 提供了 @Override 註解來標識。這個註解僅僅就是一個標識,寫與不寫都不影響。

``` public class Cat extends Animal{

@Override //重寫的方法的一個標識 public void eat(){ System.out.println("貓吃魚"); } } ```

3. 如何使用方法重寫

現有這樣一個場景:幾何圖形都有面積和周長,不同的幾何圖形,面積和周長的演算法也不一樣。矩形有長和寬,通過長和寬能夠計算矩形的面積和周長;圓有半徑,通過半徑可以計算圓的面積和周長。請使用繼承相關的知識完成程式設計。

``` package com.qf.oop.shape;

/ * 幾何圖形 */ public class Shape { / * 計算周長 * @return */ public double calculatePerimeter(){ return 0; }

/**
 * 計算面積
 * @return
 */
public double calculateArea(){
    return 0;
}

}

package com.qf.oop.shape;

/* * 矩形 / public class Rectangle extends Shape {

private int width;

private int length;

public Rectangle(int width, int length) {
    this.width = width;
    this.length = length;
}

@Override
public double calculatePerimeter() {//重寫計算周長的方法
    return (width + length) * 2;
}

@Override
public double calculateArea() {//重寫計算面積的方法
    return width * length;
}

}

package com.qf.oop.shape;

/* * 圓 / public class Circle extends Shape{

private int radius;

public Circle(int radius) {
    this.radius = radius;
}

@Override
public double calculateArea() {//重寫計算面積的方法
    return Math.PI * radius * radius;
}

@Override
public double calculatePerimeter() {//重寫計算周長的方法
    return 2 * Math.PI * radius;
}

}

package com.qf.oop.shape;

public class ShapeTest {

public static void main(String[] args) {
    Shape s1 = new Rectangle(10, 9);
    System.out.println(s1.calculatePerimeter());
    System.out.println(s1.calculateArea());


    Shape s2 = new Circle(5);
    System.out.println(s2.calculatePerimeter());
    System.out.println(s2.calculateArea());
}

} ```

重寫方法時訪問修飾符的級別不能降低。