Java基礎之面向對象三大特性:封裝、繼承和多態


封裝

封裝指的是:利用抽象數據類型(比如我們常說的類)將 數據對數據的操作 封裝在一起,使其構成一個完整的、不可分割的獨立實體,這些包裝起來的數據就構成了抽象數據類型的屬性,而對數據的操作則成爲了方法。

在整個封裝過程中,數據將會被保存在抽象數據類型的內部,並儘可能地隱藏內部的實現細節,同時提供對這些數據進行操作的接口,通過這些接口使外面的對象可以訪問和修改該對象的內部數據。

封裝有以下幾大好處:

  • 良好的封裝能夠減少耦合
  • 可以自由修改類內部的結構
  • 可以對成員數據進行精確的控制
  • 隱藏內部信息和具體實現細節

比如,封裝 Person 類型:

public class Person {

    /*
     * 對屬性進行封裝:姓名、性別、年齡
     */
    private String name;
    private String sex;
    private int age;

    /*
     * 提供對外開放的 setter()、getter() 接口
     */
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public String getSex() {
        return this.sex;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }
}

從上面可以看出,封裝把一個對象的屬性私有化,同時提供一些可以被外界訪問的屬性的方法。到這裏爲止,好像也看不出封裝的好處,那我們從程序的角度來分析封裝帶來的好處,如果我們不使用封裝,那麼該對象就沒有 setter() 和 getter(),那麼Person 類應該這樣寫:

public class Person {
    private String name;
    private String sex;
    private int age;
}

程序中就要這樣使用它:

Person person = new Person();
person.age = 24;

但是哪天我們要把年齡 age 的類型改爲 String 類型呢?只有一處使用了這個類還好,萬一有幾十甚至上百個地方使用了的話豈不是要改到崩潰?但如果我們使用了封裝的話,我們完全只需要稍微修改一下 setAge() 方法即可:

public void setAge(int age) {
	// 使用類型轉換即可
    this.age = String.valueOf(age);
}

這樣,所有使用 person.setAge(24) 的地方都保持不變。所以,到這裏我們可以看出:封裝確實可以使我們容易地修改類的內部實現,而無需修改使用了該類的客戶代碼

我們再來看一下封裝的另一個好處:對成員變量的精確控制

假如沒有封裝,我們進行年齡設置的時候,不小心設置成了這樣:

Person person = new Person();
person.age = 300;

很明顯,實際的情況下,年齡不應該出現這麼大的數字的,如果自己粗心寫錯並發現了還好,如果沒有發現或者別人使用錯了那就麻煩了。但使用封裝我們就可以避免這種類型的問題,我們可以在 age 的訪問接口進行一些控制:

public void setAge(int age) {
	// 控制年齡輸入範圍
    if (age < 0 || age > 120) {
        System.out.println("年齡設置錯誤!");
    } else {
        this.age = age;
    }
}

通過對接口數據的控制和過濾來保護成員變量從而提高程序的安全性,這是一種很好的設計思想。到這裏爲止,大家應該都體會到封裝的好處了吧。


繼承

繼承是建立在封裝之上的,有了封裝纔有繼承,繼承的目的是:複用代碼

繼承是使用已存在的類的定義作爲基礎建立新類的技術,新類的定義可以增加新的數據或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承我們能夠非常方便地複用以前的代碼,能夠大大的提高開發的效率。

繼承所描述的是 “is-a” 的關係,如果有兩個對象A和B,若可以描述爲"A is B",則可以表示A繼承B,其中B是被繼承者稱之爲父類或者超類,A是繼承者稱之爲子類或者派生類。

實際上繼承者是被繼承者的特殊化,它除了擁有被繼承者的特性外,還擁有自己獨有得特性。在繼承關係中,繼承者完全可以替換被繼承者,反之則不可以,例如:我們可以說貓是動物,但不能說動物是貓就是這個道理,我們把這個稱之爲"向上轉型"

繼承有三個重點需要牢記:

  • 子類只能擁有父類非 private 的屬性和方法
  • 子類可以擁有自己屬性和方法,即子類可以對父類進行擴展
  • 子類可以用自己的方式實現父類的方法
注意:子類只能繼承父類的非 private 的屬性和方法,父類的 private 屬性和方法只能在父類的類中操作,換句話說,private 屬性和方法只限定在類本身內部操作,無法被繼承。

當然,說到繼承,必定少不了這三個東西:構造器、protected關鍵字、向上轉型。

☕️ 構造器

子類除了不能繼承父類的 private 屬性和方法以外,父類的構造器也是無法繼承的。只能夠被調用,而不能被繼承,想調用父類的構造方法我們需要使用 super() 函數。

在繼承體系中,構造器的正確初始化非常重要,我們必須要保證一點:子類的構造器中必須調用父類的構造器來完成父類的初始化。我們看一下下面這個例子:

public class Person {

    private String name;
    private String sex;
    private int age;
    
    public Person() {
        System.out.println("construct Person...");
    }
}

public class Student extends Person {

    private int id;

    public Student() {
    	super();
        System.out.println("construct Student...");
    }

    public static void main(String[] args) {
        Student stu = new Student();
    }
}

結果輸出:
construct Person…
construct Student…

這裏,由於 Student 構造器調用的是父類的默認構造器,所以可以省略不寫,因爲編譯器會默認給子類調用父類的默認構造器,但這個前提是父類必須給出默認構造器,如果父類沒有默認構造器,就需要我們顯示的使用 super() 調用父類構造器,否則編譯器將報錯。

總結而來就是:對於繼承而已,子類會默認調用父類的構造器,但是如果沒有默認的父類構造器,子類必須要顯示的指定父類的構造器,而且必須是在子類構造器中做的第一件事(第一行代碼)。

☕️ protected 關鍵字

我們知道,類的屬性和方法有三種訪問限制修飾符:public、protected、private,其中子類可以訪問父類的非 private 屬性和方法,即子類可以訪問父類的 public 和 protected 所修飾的屬性和方法。但是,在 java 中,protected 所修飾的對象還可以被同一個包下的類所訪問

☕️ 向上轉型

我們知道繼承是is-a的相互關係,比如:貓繼承於動物,所以我們可以說貓是動物,或者說貓是動物的一種。像這種把貓看做動物的現象就是向上轉型,例子如下:

public class Animal {
	public void run() {
		System.out.println("Animal run...");
	}

	public static void showRun(Animal animal) {
		animal.run();
	}
}

public class Cat extends Animal {
	public static void main(String[] args) {
         Cat cat = new Cat();
         Animal.showRun(cat);      //向上轉型
     }
}

上面的例子中,把子類 cat 傳給父類的靜態方法 showRun(),會將子類轉換成父類,在繼承關係上面是向上移動的,所以一般稱之爲向上轉型。由於向上轉型是從一個叫專用類型向較通用類型轉換,所以它總是安全的,唯一發生變化的可能就是屬性和方法的丟失。這就是爲什麼編譯器在"未曾明確表示轉型"或"未曾指定特殊標記"的情況下,仍然允許向上轉型的原因。

☕️ 謹慎繼承

首先我們需要明確,繼承存在如下缺陷:

  • 父類變,子類就必須變
  • 繼承破壞了封裝,對於父類而言,它的實現細節對與子類來說都是透明的
  • 繼承是一種強耦合關係

所以在糾結什麼時候使用繼承或者要不要使用繼承時,我們可以參考一下《Think in java》中一種方法:問一問自己是否需要從子類向父類進行向上轉型。如果必須向上轉型,則繼承是必要的,但是如果不需要,則應當好好考慮自己是否需要繼承。


多態

我們知道,封裝隱藏了類的內部實現機制,可以在不影響使用的情況下改變類的內部結構,同時也保護了數據。對外界而言它的內部細節是隱藏的,暴露給外界的只是它的訪問方法。

而繼承是爲了重用父類代碼。兩個類若存在IS-A的關係就可以使用繼承。同時繼承也爲實現多態做了鋪墊

🍭 多態的概念:

所謂多態就是指程序中定義的引用變量所指向的具體類型和該引用變量所調用的方法在編譯時並不確定,只有在程序運行期間才確定。即:一個引用變量倒底會指向哪個類的實例對象,以及該引用變量所調用的方法到底是哪個類中實現的方法,必須在程序運行期間才能決定。

因爲在程序運行時才確定具體的類,這樣,在不修改源程序代碼的情況下,就可以讓引用變量綁定到各種不同的類實現上,從而導致該引用變量調用的具體方法隨之改變。即:不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態,這就是多態性。

多態的實現需要藉助於向上轉型這個特性,但是向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的丟失,而導致我們不能夠獲取它們。所以父類類型的引用可以調用父類中定義的所有屬性和方法,對於只存在與子類中的方法和屬性它就無法使用了。看下面的例子:

public class Person {

    public void func1() {
        System.out.println("調用 Person 的 func1");
        func2();
    }

    public void func2() {
        System.out.println("調用 Person 的 func2");
    }
}

public class Student extends Person {

    // 重載父類方法,父類不存在這個方法,向上轉型後,父類不能調用這個方法
    public void func1(String a) {
        System.out.println("調用 Student 的 func1");
        func2();
    }

    // 重寫父類方法,指向子類對象的父類引用調用func2時必定調用該方法
    public void func2() {
        System.out.println("調用 Student 的 func2");
    }
}

public class Test {
	public static void main(String[] args) {
        Person test = new Student(); // 父類引用指向子類對象
        test.func1();
    }
}

結果輸出:
調用 Person 的 func1
調用 Student 的 func2

從程序的運行結果中我們發現,test.fun1() 首先是運行父類中的 fun1().然後再運行子類中的 fun2()。

分析:在這個程序中子類重載了父類的方法 func1(),重寫了 func2(),由於重載後的 func1(String a) 和 func1() 不是同一個方法,所以父類並沒有這個方法,在向上轉型後就會丟失該方法,所以執行 test.fun1() 是無法調用 func1(String a) 方法的。而子類重寫了 func2() ,那麼在調用func2()時就會調用子類的 func2() 方法。

所以,對於多態,我們可以總結如下:

指向子類對象的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,哪怕是重載的方法。若子類重寫了父類中的某些方法,在調用該些方法的時候,必定是使用子類中定義的這些方法(動態連接、動態調用)。

Java實現多態有三個必要條件:繼承、重寫、向上轉型。

  • 繼承:在多態中必須存在有繼承關係的子類和父類
  • 重寫:子類對父類中某些方法進行重新定義,在調用這些方法時就會調用子類的方法
  • 向上轉型:在多態中需要將子類對象賦給父類引用,只有這樣該,引用才能夠調用父類的方法和子類的方法

對於Java而言,它多態的實現機制遵循一個原則:當父類對象引用變量引用子類對象時,由被引用對象的類型而不是引用變量的類型決定了該調用誰的成員方法,但是這個被調用的方法必須是在父類中定義過的,並且被子類覆蓋的方法。

在Java中有兩種形式可以實現多態:繼承和接口

🍭 基於繼承實現的多態

基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行爲。

所以基於繼承實現的多態可以總結如下:對於引用子類對象的父類引用,在處理該引用時,它適用於繼承該父類的所有子類,子類對象的不同,對方法的實現也就不同,執行相同動作產生的行爲也就不同。

如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外接口,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一接口來處理該層次的方法。

🍭 基於接口實現的多態

繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那麼就可以通過實現接口並覆蓋接口中同一方法的幾不同的類體現的。

在接口的多態中,指向接口的引用必須是實現了該接口的一個類的實例程序,在運行時,根據對象引用的實際類型來執行對應的方法。

繼承都是單繼承,只能爲一組相關的類提供一致的服務接口。但是接口可以是多繼承多實現,它能夠利用一組相關或者不相關的接口進行組合與擴充,能夠對外提供一致的服務接口。所以它相對於繼承來說有更好的靈活性。

如 Map map =new HashMap ; List list=new ArraryList 都是基於接口實現的多態。

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