物件導向程式設計三⼤特性 --封裝、繼承、多型

2022-07-21 12:02:12

封裝

把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。封裝是物件導向的特徵之一,是物件和類概念的主要特性。

通俗的說,一個類就是一個封裝了資料以及操作這些資料的程式碼的邏輯實體。在一個物件內部,某些程式碼或某些資料可以是私有的,不能被外界存取。
通過這種方式,物件對內部資料提供了不同級別的保護,以防止程式中無關的部分意外的改變或錯誤的使用了物件的私有部分。但是如果⼀個類沒有提供給外界存取的⽅法,那麼這個類也沒有什麼意義了。

我們來看一個常見的 類,比如:Student

public class Student implements Serializable {
    
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

}

將物件中的成員變數進行私有化,外部程式是無法存取的。對外提供了存取的方式,就是set和get方法。
而對於這樣一個實體物件,外部程式只有賦值和獲取值的許可權,是無法對內部進行修改

繼承

繼承 就是子類繼承父類別的特徵和行為,使得子類物件(範例)具有父類別的範例域和方法,或子類從父類別繼承方法,使得子類具有父類別相同的行為。
在 Java 中通過 extends 關鍵字可以申明一個類是從另外一個類繼承而來的,一般形式如下:

class 父類別 {
}
 
class 子類 extends 父類別 {
}

繼承概念的實現方式有二類:實現繼承介面繼承

實現繼承是指直接使用基礎類別的屬性和方法而無需額外編碼的能力
介面繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力
一般我們繼承基本類和抽象類用 extends 關鍵字,實現介面類的繼承用 implements 關鍵字。

注意點:

通過繼承建立的新類稱為「子類」或「派生類」,被繼承的類稱為「基礎類別」、「父類別」或「超類」。
繼承的過程,就是從一般到特殊的過程。要實現繼承,可以通過「繼承」(Inheritance)和「組合」(Composition)來實現。
子類可以擁有父類別的屬性和方法。
子類可以擁有自己的屬性和方法, 即⼦類可以對⽗類進⾏擴充套件。
子類可以重寫覆蓋父類別的方法。
JAVA 只支援單繼承,即一個子類只允許有一個父類別,但是可以實現多級繼承,及子類擁有唯一的父類別,而父類別還可以再繼承。

使用implements關鍵字可以變相的使java具有多繼承的特性,使用範圍為類繼承介面的情況,可以同時繼承多個介面(介面跟介面之間採用逗號分隔)。

# implements 關鍵字

public interface A {
    public void eat();
    public void sleep();
}
 
public interface B {
    public void show();
}
 
public class C implements A,B {
}

值得留意的是: 關於父類別私有屬性和私有方法的繼承 的討論
這個網上 有大量的爭論,我這邊以Java官方檔案為準:
With the use of the extends keyword, the subclasses will be able to inherit all the properties of the superclass except for the private properties of the superclass.
子類不能繼承父類別的私有屬性(事實),但是如果子類中公有的方法影響到了父類別私有屬性,那麼私有屬性是能夠被子類使用的。

官方檔案 明確說明: private和final不被繼承,但從記憶體的角度看的話:父類別private屬性是會存在於子類物件中的。

通過繼承的方法(比如,public方法)可以存取到父類別的private屬性

如果子類中存在與父類別private方法簽名相同的方法,其實相當於覆蓋

個人覺得文章裡的一句話很贊,我們不可能完全繼承父母的一切(如性格等),但是父母的一些無法繼承的東西卻仍會深刻的影響著我們。

多型

同一個行為具有多個不同表現形式或形態的能力就是 多型。網上的爭論很多,筆者個人認同網上的這個觀點:過載也是多型的一種表現,不過多型主要指執行時多型
Java 多型可以分為 過載式多型重寫式多型:

-過載式多型,也叫編譯時多型。編譯時多型是靜態的,主要是指方法的過載,它是根據參數列的不同來區分不同的方法。通過編譯之後會變成兩個不同的方法,在執行時談不上多型。也就是說這種多型再編譯時已經確定好了。
-重寫式多型,也叫執行時多型。執行時多型是動態的,主要指繼承父類別和實現介面時,可使用父類別參照指向子類物件實現。這個就是大家通常所說的多型性
這種多型通過動態繫結(dynamic binding)技術來實現,是指在執行期間判斷所參照物件的實際型別,根據其實際的型別呼叫其相應的方法。也就是說,只有程式執行起來,你才知道呼叫的是哪個子類的方法。 這種多型可通過函數的重寫以及向上轉型來實現。

多型存在的三個必要條件:

  1. 繼承
  2. 重寫
  3. 父類別參照指向子類物件:Parent p = new Child();

我們一起來看個例子,仔細品讀程式碼,就明白了:

@SpringBootTest
class Demo2021ApplicationTests {

    class Animal {
        public void eat(){
            System.out.println("動物吃飯!");
        }
        public void work(){
            System.out.println("動物可以幫助人類幹活!");
        }
    }

    class Cat extends Animal {
        public void eat() {
            System.out.println("吃魚");
        }
        public void sleep() {
            System.out.println("貓會睡懶覺");
        }
    }

    class Dog extends Animal {
        public void eat() {
            System.out.println("吃骨頭");
        }
    }

    @Test
    void contextLoads() {
        //part1
        Cat cat_ = new Cat();
        cat_.eat();
        cat_.sleep();
        cat_.work();

        //part2
        Animal cat=new Cat();
        cat.eat();
        cat.work();
        cat.sleep();//此處編譯會報錯。

        Animal dog=new Dog();
        dog.eat();//結果為:吃骨頭。此處呼叫子類的同名方法。
        
        //part3
        //如果想要呼叫父類別中沒有的方法,則要向下轉型,這個非常像"強轉"
        Cat cat222 = (Cat)cat;        // 向下轉型(注意,如果是(Cat)dog 則會報錯)
        cat222.sleep();        //結果為:貓會睡懶覺。 可以呼叫 Cat 的 sleep()
    }



}

我們來看上面part1部分:

Cat cat_ = new Cat();
cat_.eat();
cat_.sleep();
cat_.work();

結果:

吃魚
貓會睡懶覺。
動物可以幫助人類幹活!

cat_.work(); 這處繼承了父類別Animal的方法,還是很好理解的
我們接著看part2:

Animal cat=new Cat();
cat.eat();
cat.work();
cat.sleep();//此處編譯會報錯。

Animal dog=new Dog();
dog.eat();//結果為:吃骨頭。此處呼叫子類的同名方法。

這塊就比較特殊了,我們一句句來看
Animal cat=new Cat(); 像這種這個 父類別參照指向子類物件,這種現象叫做:"向上轉型",也被稱為多型的參照
cat.sleep();這句 編譯器會提示 編譯報錯。 表明:當我們當子類的物件作為父類別的參照使用時,只能存取子類中和父類別中都有的方法,而無法去存取子類中特有的方法
值得注意的是:向上轉型是安全的。但是缺點是:一旦向上轉型,子類會丟失的子類的擴充套件方法,其實就是 子類中原本特有的方法就不能再被呼叫了。所以cat.sleep()這句會編譯報錯。

cat.eat();這句的結果列印:吃魚。程式這塊呼叫我們Cat定義的方法。
cat.work();這句的結果列印:動物可以幫助人類幹活! 我們上面Cat類沒有定義work方法,但是卻使用了父類別的方法,這是不是很神奇。其實此處調的是父類別的同名方法
Animal dog=new Dog();dog.eat();這句的結果列印為:吃骨頭。此處呼叫子類的同名方法。
由此我們可以知道當發生向上轉型,去呼叫方法時,首先檢查父類別中是否有該方法,如果沒有,則編譯錯誤;如果有,再去呼叫子類的同名方法。如果子類沒有同名方法,會再次去調父類別中的該方法

我們現在知道了 向上轉型時會丟失子類的擴充套件方法,哎,但我們就是想找回來,這可咋辦?
向下轉型可以幫助我們,找回曾經失去的

我們來看part3:

    Cat cat_real = (Cat)cat;  //注意 此處的cat 對應上面父類別Animal,可不是子類
    cat_real.sleep(); 

Cat cat = (Cat)cat; cat222.sleep(); 這個向下轉型非常像"強轉"。
列印的結果:貓會睡懶覺。此處又能呼叫了 子類Cat 的 sleep()方法了。

一道簡單的面試題

我們再來看一道有意思的題,來強化理解

public class Main {
    
    static class Animal{
        int weight = 10;

        public void print() {
            System.out.println("this Animal Print:" + weight);
        }

        public Animal() {
            print();
        }
    }

    static class Dog extends Animal {
        int weight = 20;

        @Override
        public void print() {
            System.out.println("this Dog Print:" + weight);
        }

        public Dog() {
            print();
        }
    }

    public static void main(String[] args) {
        Dog dog = new Dog();

        System.out.println("---------------");
        Animal dog222 = new Dog();
        Dog dog333 =  (Dog)dog222;
        
        System.out.println("---------------");
        Dog dog444 = (Dog)new Animal();

    }
}

執行結果:

this Dog Print:0
this Dog Print:20
---------------
this Dog Print:0
this Dog Print:20
---------------
this Animal Print:10
Exception in thread "main" java.lang.ClassCastException: com.zj.Main$Animal cannot be cast to com.zj.Main$Dog
at com.zj.Main.main(Main.java:15)

做對了嘛,不對的話,複製程式碼去idea中debug看看

我們先看第一部分
Dog dog = new Dog();
程式內部的執行順序:

  1. 先 初始化 父類別Animal 的屬性 int weight=10
  2. 然後 呼叫父類別Animal的構造方法,執行print()
  3. 實際呼叫子類Dog的print()方法,列印:this Dog Print:0,由於此時的子類屬性weight 並未初始化
  4. 初始化 子類Dog 的屬性 int weight=20
  5. 呼叫 子類Dog的構造方法,執行print()
  6. 實際呼叫當前類的print()方法,列印this Dog Print:20

其中有幾處我們需要注意一下:範例化子類dog,程式會去預設優先範例化父類別,即子類範例化時會隱式傳遞Dog的this呼叫父類別構造器進行初始化工作,這個和JVM的雙親委派機制有關,這裡就不展開講了,先挖個坑,以後再來填