一篇文章快速瞭解Java中的繼承與多態

一. 繼承

1.介紹

多個類中存在相同屬性和行爲時,將這些內容抽取到單獨一個類中,那麼多個類無需再定義這些屬性和行爲,只要繼承那個類即可。此處的多個類稱爲子類(此類,派生類,拓展類),單獨的這個類稱爲父類(基類 ,超類)。可以理解爲:“子類 is a 父類”。

類繼承語法規則:
class Subclass extends SuperClass{ };

以下幾點需要注意:

  • 子類繼承了父類所有可以訪問的數據域和方法
  • 父類中聲明爲private的屬性或方法,子類繼承父類以後,仍然認爲獲取了父類中私的結構。只因爲封裝性的影響,使得子類不能直接調用父類的結構而已。如果父類中定義了公共的訪問器 / 修改器,那麼可以通過這些公共的訪問器 / 修改器來訪問和修改它們。
    在這裏插入圖片描述
  • 子類並不是父類的一個子集。實際上,一個子類通常比它的父類包含更多的信息和方法
  • Java 中是不允許多重繼承的。一個 Java 類只可能直接繼承自一個父類。這種限制稱爲單一繼承( single inheritance)。如果使用 extends 關鍵字來定義一個子類,它只允許有一個父類。然而,多重繼承是可以通過接口來實現。
    在這裏插入圖片描述

2.super關鍵字

在Java類中使用super來調用父類中的指定操作:

  • 用於訪問父類中定義的屬性(屬性沒有私有化)
  • 用於調用父類中定義的成員方法
  • 用於在子類構造器中調用父類的構造器

2.1 調用父類的構造方法

構造方法用於構建一個類的實例。不同於屬性和普通方法,父類的構造方法不會被子類繼承。它們只能使用關鍵字 super 從子類的構造方法中調用。

調用父類構造方法的語法是:
super()或者super(parameters);

  • 語句 super() 調用父類的無參構造方法,而語句 super(arguments) 調用與參數匹配的父類的構造方法。語句 super() 和 super (arguments) 必須出現在子類構造方法的第一行,這是顯式調用父類構造方法的唯一方式。
  • 子類中所有的構造器默認都會訪問父類中空參數的構造器
  • 如果子類構造器中既未顯式調用父類或本類的構造器,且父類中又沒有無參的構造器,則編譯出錯.所以呢,一般情況下,最好能爲每個類提供一個無參構造方法,以便於對該類進行擴展,同時避免錯誤。
  • 特殊情況:當子類和父類中定義了同名的屬性時,我們要想在子類中調用父類中聲明的屬性,則**必須顯式的使用"super.屬性"**的方式,表明調用的是父類中聲明的屬性。
  • 特殊情況:當子類重寫了父類中的方法以後,我們想在子類的方法中調用父類中被重寫的方法時,則必須顯式的使用"super.方法"的方式,表明調用的是父類中被重寫的方法。
public class Person {
    private String name;
    private int age;
    private Date birthDate;
    public Person(String name, int age, Date d) {
        this.name = name;
        this.age = age;
        this.birthDate = d; }
    public Person(String name, int age) {
        this(name, age, null);
    }
    public Person(String name, Date d) {
        this(name, 30, d);
    }
    public Person(String name) {
        this(name, 30);
    }
}

public class Student extends Person {
    private String school;
    public Student(String name, int age, String s) {
        super(name, age);
        school = s; }
    public Student(String name, String s) {
        super(name);
        school = s; }
    // 編譯出錯: no super(),系統無法調用父類無參數的構造器。
    //There is no default constructor available in 'chapter01.Person'
    public Student(String s) {
        school = s; }
}

在這裏插入圖片描述

2.2 子類實例化的過程

在任何情況下,構造一個類的實例時,將會調用沿着繼承鏈的所有父類的構造方法當構造一個子類的對象時,子類構造方法會在完成自己的任務之前,首先調用它的父類的構造方法。如果父類繼承自其他類,那麼父類構造方法又會在完成自己的任務之前,調用它自己的父類的構造方法。這個過程持續到沿着這個繼承體系結構的最後一個構造方法被調用爲止。這就是構造方法鏈(constructor chaining)。

public class Faculty extends Employee {
    public static void main(String[] args) {
        Faculty faculty = new Faculty();
    }

    public Faculty() {
        System.out.println("(4) Performs Faculty's tasks");
    }
}

class Employee extends Person {
    public Employee() {
        this("(2)Invoke Employee's overloaded constructor");
        System.out.println("(3)Performs Employee's tasks ");
    }

    public Employee(String s) {
        System.out.println(s);
    }
}

class Person {
    public Person() {
        System.out.println("(1) Performs Person's tasks");
    }
}

結果:
在這裏插入圖片描述
在第3 行,new Faculty() 調用Faculty 的無參構造方法。由於 Faculty 是 Employee 的子類,所以,在Faculty 構造方法中的所有語句執行之前,先調用 Employee 的無參構造方法。Employee 的無參構造方法調用Employee 的第二個構造方法(第13 行)。由於 Employee 是 Person 的子類,所以,在 Employee 的第二個構造方法中所有語句執行之前,先調用 Person 的無參構造方法。
在這裏插入圖片描述

2.3 調用父類的方法

super.方法名(參數);

3.方法重寫

子類從父類中繼承方法。有時,子類需要修改父類中定義的方法的實現,這稱作方法重
寫(method overriding),:要重寫一個方法,需要在子類中使用和父類一樣的簽名以及一樣的返回值類型來對該方法進行定義。

注意以下幾點:

  1. 僅當實例方法是可訪問時,它才能被覆蓋。因爲私有方法在它的類本身以外是不能訪問的,所以它不能被覆蓋。如果子類中定義的方法在父類中是私有的,那麼這兩個方法完全沒有關係。
  2. 子類不能用語法 super.super.toStringO 訪問父類的父類中的toString ,這是一個語法錯誤。
  3. 與實例方法一樣,靜態方法也能被繼承。但是,靜態方法不能被覆蓋。如果父類中定義的靜態方法在子類中被重新定義,那麼在父類中定義的靜態方法將被隱藏。可以使用語法:父類名 .靜態方法名(SuperClassName.staticMethodName) 調用隱藏的靜態方法。

這個再次詳細說明一下

public class StaticExtends {

    public static void main(String[] args) {
        //聲明爲Father類,然後son1靜態方法和Father類綁定
        Father son = new Son();
        
        son.method();
        son.staticMethod();

        Son son2 = new Son();
        son2.method();
        son2.staticMethod();
    }
}

class Father {

    void method() {
        System.out.println("父類方法");
    }

    static void staticMethod() {
        System.out.println("父類靜態方法");
    }
}

class Son extends Father {

    @Override
    void method() {
        System.out.println("子類方法");
    }

    static void staticMethod() {
        System.out.println("子類靜態方法");
    }
}

輸出結果:
在這裏插入圖片描述
在子類中重寫父類的static方法,是不會報錯的,編譯也可以通過,但是在通過一個聲明爲父類,實際類型爲子類的引用變量調用該方法時,發現被調用的仍是父類中原本以爲會被覆蓋的方法,不具有“多態”特性。所以呢,父類的static方法是不會被重寫的。

4.Object類及其常用方法

Java 中的所有類都繼承自 java.lang.Object 類,如果在定義一個類時沒有指定繼承性,那麼這個類的父類就被默認爲是 Object。
在這裏插入圖片描述

4.1 toString()方法

Object 類中toString()方法的默認實現是:

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
     }

調用一個對象的 toString() 會返回一個描述該對象的字符串。默認情況下,它*返回一個由該對象所屬的類名、at 符號(@)以及該對象十六進制形式的內存地址組成的字符串。*這個信息不是很有用,所以重寫。

  • 像String、Date、File、包裝類等都重寫了Object類中的toString()方法。使得在調用對象的toString()時,返回"實體內容"信息。
  • 可以根據需要在用戶自定義類型中重寫toString()方法。 如String 類重寫了toString()方法,返回字符串的值。
s1=“hello”;
System.out.println(s1);//相當於System.out.println(s1.toString());
  • 基本類型數據轉換爲String類型時,調用了對應包裝類的toString()方法
int a=10; 
System.out.println(“a=+a);

4.2 equals()方法

Object 類中 equals 方法的默認實現是:

public boolean equals(Object obj) {
return (this obj); 
}

說明:Object類中定義的equals()和==的作用是相同的:比較兩個對象的地址值是否相同.即兩個引用是否指向同一個對象實體。

  • 像String、Date、File、包裝類等都重寫了Object類中的equals()方法。重寫以後,比較的不是兩個引用的地址是否相同,而是比較兩個對象的"實體內容"是否相同。
  • 通常情況下,我們自定義的類如果使用equals()的話,也通常是比較兩個對象的"實體內容"是否相同。那麼,我們 就需要對Object類中的equals()進行重寫。重寫的原則:比較兩個對象的實體內容是否相同

tips:==和equals的區別:

  • 1 == 既可以比較基本類型也可以比較引用類型。對於基本類型就是比較值,對於引用類型就是比較內存地址
  • 2 equals的話,它是屬於java.lang.Object類裏面的方法,如果該方法沒有被重寫過默認也是;我們可以看到String等類的equals方法是被重寫過的,而且String類在日常開發中用的比較多,久而久之,形成了equals是比較值的錯誤觀點。
  • 3 具體要看自定義類裏有沒有重寫Object的equals方法來判斷。
  • 4 通常情況下,重寫equals方法,會比較類中的相應屬性是否都相等。

5. 防止拓展與重寫 —final關鍵字

在Java中聲明類、變量和方法時,可使用關鍵字final來修飾,表示“最終的”。

  1. final標記的類不能被繼承。 提高安全性,提高程序的可讀性。
- String類、System類、StringBuffer類 
  1. final標記的方法不能被子類重寫。
    • 比如:Object類中的getClass()。
  2. final標記的變量(成員變量或局部變量)即稱爲常量。名稱大寫,且只能被賦值一次。
    • final double MY_PI = 3.14;
    • final標記的成員變量必須在聲明時或在每個構造器中或代碼塊中顯式賦值,然後才能使用。
public final class Test {
public static int totalNumber = 5;
public final int ID;
public Test() {
ID = ++totalNumber; // 可在構造器中給final修飾的“變量”賦值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
final int I = 10;
final int J; J = 20;
J = 30; // 非法
}
 }
  
  

二. 多態

6.1 介紹

首先呢,我們知道繼承關係使一個子類繼承父類的特徵,並且附加一些新特徵。子類是它的父類的特殊化,每個子類的實例都是其父類的實例,但是反過來就不成立。例如:每個圓都是一個幾何對象,但並非每個幾何對象都是圓。因此,總可以將子類的實例傳給需要父類型的參數。使用父類對象的地方都可以使用子類的對象。這就是通常所說的多態。簡單來說,多態意味着父類型的變量可以引用子類型的對象。

6.2 動態綁定

我們都知道方法可以在父類中定義而在子類中重寫。(方法可以在沿着繼承鏈的多個類中實現。JVM 決定運行時調用哪個方法。)那麼

Object o = new SonObject();
System.out.println(o.toSting);

這裏的 o 調用哪個 tostring() 呢?
我們首先介紹兩個術語:聲明類型和實際類型。**一個變量必須被聲明爲某種類型。變量的這個類型稱爲它的聲明類型(declared type)。**這裏,o 的聲明類型是 Object。一個引用類型變量可以是一個 null 值或者是一個對聲明類型實例的引用。實例可以使用聲明類型或它的子類型的構造方法創建。變量的實際類型(actual type) 是被變量引用的對象的實際類。這裏,o 的實際類型是SonObject, 因爲 o 指向使用 new SonObject() 創建的對象。o 調用哪個toString() 方法由 o 的實際類型決定。這稱爲動態綁定(dynamic binding)。
也就是多態情況下,編譯時,看左邊;運行時,看右邊。
“看左邊”:看的是父類的引用(父類中不具備子類特有的方法)
“看右邊”:看的是子類的對象(實際運行的是子類重寫父類的方法)

動態綁定工作機制如下:假設對象 o 是類 Cl, C2, … ,Cn-1, Cn 的實例,其中 C1是 C2的子類,C2 是 C3 的子類,… ,Cn-1是 Cn 的子類。也就是說,Cn 是最通用的類,C1是最特殊的類。在 Java 中,Cn 是 Object 類。如果對象 o 調用一個方法 p, 那麼JVM 會依次在類 Cl,C2, … ,Cn-1,Cri 中查找方法 p 的實現,直到找到爲止一旦找到一個實現,就停止査找,然後調用這個首先找到的實現。
在這裏插入圖片描述
看以下代碼:

public class DynamicBindDemo {
    public static void main(String[] args) {
        m(new GraduateStudent());
        m(new Student());
        m(new Person());
        m(new Object());
    }

    public static void m(Object x) {
        System.out.println(x.toString());
    }
}

class GraduateStudent extends Student {

}

class Student extends Person {
    @Override
    public String toString() {
        return "Student";
    }
}

class Person {
    @Override
    public String toString() {
        return "Person";
    }
}

輸出結果:
在這裏插入圖片描述

6.3 對象轉換和instanceof()運算符

6.3.1 對象轉換

對象的引用可以類型轉換爲對另外一種對象的引用,這稱爲對象轉換。

Object o = new Student();
m(o);

Student 的實例也是 Object 的實例,所以,語句 Object o = new StudentO 是合法的,它稱爲隱式轉換(implicit casting)。
但是Student b = o;將會發生編譯錯誤,原因是 Student 對象總是 Object 的實例,但是,Object 對象不一定是 Student 的實例。即使可以看到 0實際上是一個 Student 的對象,但是編譯器還沒有聰明到知道這一點。爲了告訴編譯器o就是一個 Student 對象,就要使用顯式轉換( explicit casting)。

Student b = (Student)o;// Explicit casting

總是可以將一個子類的實例轉換爲一個父類的變量,稱爲向上轉換(upcasting),因爲子類的實例永遠是它的父類的實例。當把一個父類的實例轉換爲它的子類變量(稱爲向下轉換(downcasting))時,必須使用轉換記號 “(子類名)” 進行顯式轉換,向編譯器表明你的意圖。爲使轉換成功,必須確保要轉換的對象是子類的一個實例。如果父類對象不是子類的一個實例,就會出現一個運行異常 ClassCastException。

6.3.2 instanceof運算符

在嘗試轉換之前確保該對象是另一個對象的實例,可以利用運算符instanceof 來實現的。

Objecto = new Circle();
if (o instanceof Circle){
Circle c = ((Circle)o;
System.out.println("The circle diameter is + o.getDiameter());
  • ① a instanceof A:判斷對象a是否是類A的實例。如果是,返回true;如果不是,返回false。
  • ② 如果 a instanceof A返回true,則 a instanceof B也返回true.其中,類B是類A的父類。
  • ③ 要求a所屬的類與類A必須是子類和父類的關係,否則編譯錯誤。

變量 myObject 被聲明爲 Object。**聲明類型
決定了在編譯時匹配哪個方法。**使用 myObject.getDiameter()會引起一個編譯錯誤,因爲Object 類沒有 getDiameter 方法。編譯器無法找到和 myObject.getDiameter()匹配的方法。所以,有必要將 myObject 轉換成 Circle 類型,來告訴編譯器 myObject 也是 Circle 的一個實例。同時要注意,引用變量 o和 c指向同一個對象,而在進行基本數據類型轉換時,會創建一個新的對象。

爲什麼沒有在一開始就把 myObject定義爲 Circle 類型呢?爲了能夠進行通用程序設計,一個好的經驗是把變童定義爲父類型,這樣,它就可以接收任何子類型的值。
在這裏插入圖片描述

注意::對象成員訪問運算符( .)優先於類型轉換運算符。使用圓括號保證在點運算符( .)之前進行轉換,例如:((Circle)object).getArea();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章