Java物件導向三大特性(封裝、繼承、多型)

2021-05-23 08:00:15


前言

OOP 語言:也就是物件導向程式設計。
物件導向的語言有三大特性:封裝、繼承、多型。三大特性是物件導向程式設計的核心。下面就來介紹一下物件導向的三大特性。
如果想了解物件導向可以看一下這一篇部落格類和物件


一、封裝

1. 封裝的概念

在我們寫程式碼的時候經常會涉及兩種角色: 類的實現者和類的呼叫者

封裝的本質就是讓類的呼叫者不必太多的瞭解類的實現者是如何實現類的,
把屬性和動作隱藏,只提供相應的方法來呼叫即可,只要知道如何使用類就行了.
當類的實現者把內部的邏輯發生變化時,類的呼叫者根本不用因此而修改方法。
這樣就降低了類使用者的學習和使用成本,從而降低了複雜程度,也保證了程式碼的安全性

2. private實現封裝

private 存取限制修飾符,被它修飾的欄位或者方法就只能在當前類中使用。

如果我們直接使用public修飾欄位

class People{
    public String name;
    public int age;
}
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.name = "小明";
        people.age = 18;
        System.out.println("姓名:"+people.name+" 年齡:"+people.age);
    }
}

執行結果

在這裡插入圖片描述
這樣的程式碼必須要了解 People 這個類的才能類內部的實現, 才能夠使用這個類. 學習成本較高。
而且一旦類的實現者把name這兩個欄位修改成myName,外部就無法呼叫了,那麼類的呼叫者就需要大量的修改程式碼,維護成本就非常高了。

使用 private 封裝屬性, 並提供 public 方法供類的呼叫者使用.

class People{
    private String name;
    private int age;
    
    public void show() {
        System.out.println("姓名:"+name+" 年齡:"+age);
    }
}
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.show();
    }
}

此時欄位已經使用 private 來修飾. 類的呼叫者(main方法中)不能直接使用. 而需要藉助 show 方法.
此時類的使用者就不必瞭解 Person 類的實現細節. 同時如果類的實現者修改了欄位的名字,
類的呼叫者不需要做出任何修改(類的呼叫者根本存取不到 name, age這樣的欄位).

那麼問題來了,我們前面說過 private 修飾的欄位只能在當前類中使用。也就是說現在我們存取不到了name和age了。這就得用到 ger 和 set 方法了

3. getter和setter方法

當我們用private修飾欄位後,這個欄位就無法被直接使用了。
這個時候就用到了 get 和 set 方法了

程式碼範例:

class People{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

    public void show() {
        System.out.println("姓名:"+name+" 年齡:"+age);
    }
}
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.setName("小明");
        people.setAge(18);
        people.show();
    }
}

執行結果

在這裡插入圖片描述

getName 即為 getter 方法, 表示獲取這個成員的值.
setName 即為 setter 方法, 表示設定這個成員的值
不是所有的欄位都一定要提供 setter / getter 方法, 而是要根據實際情況決定提供哪種方法.

在 IDEA中快速生成 get 和 set 方法
Alt+Insert 鍵或者點滑鼠右建找到Generate
在這裡插入圖片描述

4.封裝的好處

1.提高了資料的安全性
別人不能夠通過 變數名來修改某個私有的成員屬性
2.操作簡單
封裝後,類的呼叫者在使用的時候,只需呼叫方法即可。
3.隱藏了實現
實現過程對類的呼叫者是不可見的,類的呼叫者只需呼叫方法即可,不知道具體實現。

二、繼承

1. 繼承的概念

繼承的意義:程式碼的重複使用

程式碼中建立的類, 主要是為了抽象現實中的一些事物(包含屬性和方法).
有的時候客觀事物之間就存在一些關聯關係, 那麼在表示成類和物件的時候也會存在一定的關聯。

來看一段程式碼:

class Animal {
    public String name;

    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
    }
}
class Dog {
    public String name;
        
    public void eat() {
        System.out.println(this.name+"吃東西");
    }
}
class Bird {
    public String name;
    
    public void eat() {
        System.out.println(this.name+"吃東西");
    }
    
    public void fly() {
        System.out.println(this.name+"起飛");
    }
}

這個程式碼我們發現其中存在了大量的冗餘程式碼.
仔細分析, 我們發現 Animal 和 Cat 以及 Bird 這幾個類中存在一定的關聯關係。

  • 這三個類都有相同的eat方法
  • 這三個類都有一個name屬性
  • 從邏輯上講, Cat 和 Bird 都是一種 Animal (is - a語意).

此時我們就可以讓 Cat 和 Bird 分別繼承 Animal 類, 來達到程式碼重用的效果

2. extends實現繼承

基本語法

class 子類 extends 父類別 {

} 
  • 使用 extends 指定父類別.
  • Java不同於C++/Python,JAVA中一個子類只能繼承一個父類別(單繼承)
  • 子類會繼承父類別的所有public 的欄位和方法.
  • 對於父類別的 private 的欄位和方法, 子類中是無法存取的.
  • 子類的範例中, 也包含著父類別的範例. 可以使用 super 關鍵字得到父類別範例的參照

我們再把上面的程式碼修改一下,用extends關鍵字實現繼承,此時我們讓 Cat 和 Bird 繼承自 Animal 類, 那麼 Cat 在定義的時候就不必再寫 name 欄位和 eat 方法。

class Animal {
    public String name;


    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}
class Dog extends Animal {

}
class Bird extends Animal{

    public void fly() {
        System.out.println(this.name+"起飛");
    }
}
public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "金毛";
        dog.eat();
    }
}

執行結果

在這裡插入圖片描述

此時, Animal 這樣被繼承的類, 我們稱為 父類別 , 基礎類別超類, 對於像 Cat 和 Bird 這樣的類, 我們稱為 子類,或者派生類
和現實中的兒子繼承父親的財產類似, 子類也會繼承父類別的欄位和方法, 以達到程式碼重用的效果

此時我們來簡單看一下記憶體中的儲存
在這裡插入圖片描述

3. super 關鍵字

我們在類和物件講過當一個類沒有寫構造方法的時候,系統預設會有一個沒有引數且沒有任何內容的構造方法。

來看一個列子:

在這裡插入圖片描述
當我們自己給父類別寫了一個構造方法後,兩個子類都報錯了,是什麼原因呢?

因為當子類繼承父類別後,在構造子類之前,就必須先幫父類別進行構造。(重點)

就用到了關鍵字super
super 表示獲取到父類別範例的參照.,和this類似共有三種用法

1.super.父類別的成員變數
2.super.父類別的成員方法
3.super():呼叫父類別的構造方法

注意:super 和 this一樣不能在靜態方法裡使用 !

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}
class Bird extends Animal{
    public String name = "烏鴉";
    public Bird(String name) {
        super(name);// 使用 super 呼叫父類別的構造方法
    }
    public void fly() {
        System.out.println(super.name);//呼叫父類別的成員變數
        super.eat();//呼叫父類別的構造方法
        System.out.println(this.name+"起飛");//呼叫自己的成員變數
    }
}
public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("麻雀");
        bird.fly();
    }
}

執行結果

在這裡插入圖片描述
當子類和父類別有了同名的成員變數的記憶體結夠圖

在這裡插入圖片描述
注意:在用super關鍵字在子類的構造方法裡幫父類別構造的時候一定要在第一行
在這裡插入圖片描述

Object

如果一個類沒有指定父類別的時候,預設繼承的就是Object類。

class Animal {//預設繼承Object類
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}

4.存取許可權

(1) private

當我們把父類別的存取許可權改成 private 的時候,子類就無法存取了。但並不是沒有繼承,而是無法直接存取了,因為被 private 修飾的只能在當前類裡使用!

在這裡插入圖片描述
private是可以修飾構造方法的,在類外不能範例化物件,要提供一個靜態方法來幫助構造一個物件。這樣的操作在以後的單例設計模式會用到。

在這裡插入圖片描述

(2) protected

剛才我們發現, 如果把欄位設為 private, 子類不能存取. 但是設成 public, 又違背了我們 「封裝」 的初衷.兩全其美的辦法就是 protected 關鍵字

  • 對於類的呼叫者來說, protected 修飾的欄位和方法是不能存取的
  • 對於類的 子類 和 同一個包的其他類 來說, protected修飾的欄位和方法是可以存取的

在這裡插入圖片描述

在這裡插入圖片描述

(3) default

當一個類什麼修飾符都不加的時候就是預設的存取許可權,也就是包存取許可權default .相當於這個類只能在當前包中使用。

class Cat extends Animal{//沒有任何存取許可權修飾符
    Cat(String name) {
        super(name);
        this.name = name;
    }
}

(4) 小結

總結: Java 中對於欄位和方法共有四種存取許可權

1.private: 類內部能存取, 類外部不能存取
2.預設(也叫包存取許可權): 類內部能存取, 同一個包中的類可以存取, 其他類不能存取.
3.protected: 類內部能存取, 子類和同一個包中的類可以存取, 其他類不能存取.
4.public : 類內部和類的呼叫者都能存取

在這裡插入圖片描述

5.更復雜的繼承

這樣的繼承方式稱為多層繼承, 即子類還可以進一步的再派生出新的子類.
雖然語法上可以繼承很多層,但不建議超過三層,超過三層的話就用final修飾最後一層,如果再往下繼承的話編譯器就會報錯。

class Animal {
    public String name;
    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}
class B extends Animal {

}
class C extends B {

}
final class D extends C {

}

6.final 關鍵字

1.final修飾變數(常數,這個常數不能再被修改)
2.final修飾類,密封類:當前類不能再繼承
3.final修飾方法,密封方法:該方法不能進行重寫

三、組合

和繼承類似, 組合也是一種表達類之間關係的方式, 也是能夠達到程式碼重用的效果.
例如表示一個學校:

public class Student {

}
public class Teacher {

}
public class School {
 public Student[] students;
 public Teacher[] teachers;
} 

組合並沒有涉及到特殊的語法(諸如 extends 這樣的關鍵字), 僅僅是將一個類的範例作為另外一個類的欄位.
這是我們設計類的一種常用方式之一.

組合表示 has - a 語意 在剛才的例子中, 我們可以理解成一個學校中 「包含」 若干學生和教師.

繼承表示 is - a 語意 在上面的 「動物和貓」 的例子中, 我們可以理解成一隻貓也 「是」 一種動物

一定要理解組合和繼承的區別

四、多型

1. 向上轉型

(1) 概念

向上轉型就是把一個子類參照給一個父類別參照,也就是父類別參照 參照了子類的物件

class Animal {
    public String name;
    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}
class Cat extends Animal {
    
}
public class Test extends TestDemo {

    public static void main(String[] args) {
        //父類別參照 參照了 子類參照所參照的物件
        Cat cat = new Cat();
        Animal animal = cat;//向上轉型
    }
}

我們把一個 Animal型別參照了它的子類Cat這就是向上轉型

(2) 向上轉型發生的幾種時機

1.直接賦值

public static void main(String[] args) {
        //父類別參照 參照了 子類參照所參照的物件
        Animal animal = new Cat();;//向上轉型
}

2.方法傳參

我們這裡把一個 Cat的子類 傳給 一個Animal型別的父類別,這裡也是能發生向上轉型的

public class Test extends TestDemo {

    public static void func(Animal animal) {
        
    }
    public static void main(String[] args) {
        //父類別參照 參照了 子類參照所參照的物件
        Cat cat = new Cat();
        func(cat);
    }
}

3.方法返回

這裡func方法的返回型別是 Animal 但返回的確是一個Cat型別,這裡也是發生了向上轉型

public class Test extends TestDemo {
    public static Animal func() {
        Cat cat = new Cat();
        return cat;
    }
    public static void main(String[] args) {
        Animal animal = func();
    }
}

(3) 注意事項

注意:當發生向上轉型的時候,通過父類別參照只能呼叫父類別的自己的方法和成員變數

在這裡插入圖片描述

2.向下轉型

(1) 概念

知道了向上轉型,那麼向下轉型就好理解了。向下轉型就是父類別物件轉成子類物件。

我們把一個父類別參照 Animal型別的參照 給了一個 Bird型別 的參照,這就是向下轉型

注意:向下轉型的時候一定要進行強制型別轉換

class Animal {
    public String name;
    public void eat() {
        System.out.println(this.name + " 正在吃");
    }
}
class Cat extends Animal {

}
class Bird extends Animal {
    public int age;
    public void fly() {
        System.out.println(this.name+"起飛");
    }
}
public class Test extends TestDemo {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Bird bird = (Bird) animal;//必須進行強制型別轉換
    }
}

(2) instanceof關鍵字

向下轉型我們一般不建議使用,因為它非常不安全。

來看一段程式碼:

在這裡插入圖片描述
執行結果:

在這裡插入圖片描述
執行之前並沒有報出,但執行之後這裡報出了一個型別轉換異常。

因為這裡Animal本身參照的就是一個Cat的物件,然後把它強轉為Bird,因為Cat里根本沒有fly()方法,就相當於你讓一隻貓去飛,它能非起來嗎?

所以向下轉型非常的不安全,如果要讓它安全就要加上一個關鍵字instanceof 來判斷一下。

public class Test extends TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird();
        if (animal instanceof Bird) {
            Bird bird = (Bird) animal;
            bird.fly();
        }
    }
}

instanceof 可以判定一個參照是否是某個類的範例. 如果是, 則返回 true. 這時再進行向下轉型就比較安全了

所以向下轉型我們一般不建議使用,如果非要使用就一定要用instanceof 關鍵字判斷一下。

3.動態繫結(執行時繫結)

(1) 動態繫結概念

動態繫結發生的前提

1.先向上轉型
2.通過父類別參照來呼叫父類別和子類同名的覆蓋方法

來看一段程式碼:

在這裡插入圖片描述
執行結果

在這裡插入圖片描述

我們發現這裡我們通過父類別參照呼叫了 Animal 和 Cat
同名的覆蓋方法(重寫),執行的是子類Cat的eat方法。此時這裡就發生了動態繫結

動態繫結也就叫執行時繫結,因為程式在編譯的時候呼叫的其實是父類別的 eat 方法,但是程式在執行時執行的則是子類的 eat 方法,執行期間發生了繫結。

(2) 重寫(Override)

前面的部落格中我們提到了過載,那麼重寫又是什麼時候發生的呢?

重寫發生的條件

1.方法名相同
2.方法的參數列相同(返回型別和資料型別)
3.方法的返回值相同

返回值構成父子類關係也是可以發生重寫的,此時叫做:協變型別

在這裡插入圖片描述

注意:

1.子類的重寫的這個方法,他的存取修飾符,一定要大於等於父類別方法的存取修飾符
2.被final和static修飾的方法是不能發生重寫的

在這裡插入圖片描述

(3) @Override註解

被@Override註解修飾的方法代表是要重寫的方法,一但方法被這個註解修飾,只要方法的方法名、放回值、參數列有一個地方不滿足重寫的要求,編譯器就會報錯。

這個註解可以幫助開發人員進行檢查

在這裡插入圖片描述

(4) 動態繫結的一個坑

來看一段程式碼,我們範例化一個Cat類,因為Cat是子類,所以要幫父類別先構造,那麼在父類別的構造方法裡有一個 eag 方法,那麼會執行哪個類裡的 eag 方法呢?

在這裡插入圖片描述
執行結果

在這裡插入圖片描述
我們發現這裡呼叫的不是 是Animal 的 eat 方法,而是 Cat 的,因為這裡也發生了動態繫結。

所以構造方法當中也是可以發生動態繫結的
注意:這樣的程式碼以後不要輕易寫出來!

4.多型

(1) 理解多型

多型其實就是一種思想,一個事物表現出不同的形態,就是多型。

通過程式碼來理解,這裡我要列印一些形狀

class Shape {
    public void draw() {

    }
}

class Rect extends Shape{

    public void draw() {
        System.out.println("♦");
    }
}

class Cycle extends Shape{
    public void draw() {
        System.out.println("●");
    }
}

class Flower extends Shape{
    public void draw() {
        System.out.println("❀");
    }
}

class Triangle extends Shape{
    public void draw() {
        System.out.println("△");
    }
}

public class Test {
    public static void main(String[] args) {
        Shape shape = new Rect();
        shape.draw();
        Shape shape1 = new Cycle();
        shape1.draw();
        Shape shape2 = new Flower();
        shape2.draw();
        Shape shape3 = new Triangle();
        shape3.draw();
    }
}

執行結果

在這裡插入圖片描述
這不就是動態繫結嗎?和多型有什麼關係嗎?
當我們在這個程式碼中新增一個drawMap方法後
在這裡插入圖片描述
執行結果

在這裡插入圖片描述
這不就是動態繫結嗎?

我們細看會發現這是同樣一個參照呼叫同樣一個方法,能表現出不同的形態,這不就是多型思想?其實多型用到的就是動態繫結。

在這個程式碼中, 前面的程式碼是 類的實現者 編寫的, Test這個類的程式碼是 類的呼叫者 編寫的.

當類的呼叫者在編寫 drawMap 這個方法的時候, 引數型別為 Shape (父類別), 此時在該方法內部並不知道, 也不關注當 前的shape 參照指向的是哪個型別(哪個子類)的範例. 此時 shape 這個參照呼叫 draw 方法可能會有多種不同的表現(和 shape
對應的範例相關), 這種行為就稱為 多型

(2) 多型的好處

1.類呼叫者對類的使用成本進一步降低.
封裝是讓類的呼叫者不需要知道類的實現細節.
多型能讓類的呼叫者連這個類的型別是什麼都不必知道, 只需要知道這個物件具有某個方法即可
2. 可延伸能力更強
如果要新增一種新的形狀, 使用多型的方式程式碼改動成本也比較低
對於類的呼叫者來說(drawShapes方法), 只要建立一個新類的範例就可以了, 改動成本很低


總結

1.封裝:安全性
2.繼承:為了程式碼的複用(java是單繼承)
3.多型:一個事物表現出不同的形態
4.注意過載和重寫的區別
5.注意this和super的區別