『Java 語法基礎』物件導向有哪些特性

BNTang發表於2024-03-11

物件導向程式設計(OOP) 是一個將現實世界抽象為一系列物件的程式設計正規化,這些物件透過訊息傳遞機制來互相交流和協作。

OOP 的主要特性包括四個基本概念:封裝(Encapsulation)、繼承(Inheritance)、多型(Polymorphism)以及抽象(Abstraction)。

封裝(Encapsulation)

封裝是一種將資料(屬性)和行為(方法)繫結在一起的方法。

透過封裝,可以隱藏物件的具體實現細節,僅暴露出有限的介面供外界訪問。

優勢

封裝的優勢:

  • 增強安全性:隱藏內部實現細節,防止外部直接訪問物件內部的資料,減少因誤用導致的錯誤

這裡我編寫了一個 UserCredentials 類,來進行演示一下 增強安全性,分別體現在什麼地方,程式碼如下:

/**
 * @author BNTang
 * @description 使用者憑證類
 */
public class UserCredentials {
    // 私有屬性,外部無法直接訪問
    private String username;
    private String password;

    // 公有的建構函式,用於初始化使用者名稱和密碼
    public UserCredentials(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // 公有的方法,用於驗證密碼是否正確
    public boolean authenticate(String inputPassword) {
        return inputPassword != null && inputPassword.equals(this.password);
    }

    // 獲得使用者名稱的公有方法
    public String getUsername() {
        return this.username;
    }

    // 重置密碼的方法,增加安全性校驗
    public void resetPassword(String oldPassword, String newPassword) {
        if (authenticate(oldPassword)) {
            this.password = newPassword;
            System.out.println("密碼重置成功。");
        } else {
            System.out.println("舊密碼不正確,密碼重置失敗。");
        }
    }

    // 私有的設定密碼方法,外部無法訪問
    private void setPassword(String password) {
        this.password = password;
    }
}

在我提供的 UserCredentials 類的程式碼中,隱藏內部實現細節、防止外部直接訪問物件內部的資料以及減少因誤用導致的錯誤的概念都得到了實現。

  1. 隱藏內部實現細節
private void setPassword(String password) {
    this.password = password;
}

setPassword 方法是私有的 (private),意味著它只能在類內部被呼叫。外部程式碼不能直接呼叫此方法來設定密碼,這正是隱藏內部實現細節的體現。

  1. 防止外部直接訪問物件內部的資料
private String username;
private String password;

這段程式碼中,使用者名稱 (username) 和密碼 (password) 被宣告為私有變數 (private),這意味著它們不能從類的外部直接訪問,只能透過類提供的公有方法(如構造方法、getUsernameauthenticateresetPassword 方法等)來間接訪問或修改。這種機制有效地保護了類的內部資料。

  1. 減少因誤用導致的錯誤
public void resetPassword(String oldPassword, String newPassword) {
    if (authenticate(oldPassword)) {
        this.password = newPassword;
        System.out.println("密碼重置成功。");
    } else {
        System.out.println("舊密碼不正確,密碼重置失敗。");
    }
}

resetPassword 方法中,透過 authenticate 方法校驗舊密碼是否正確,只有在舊密碼正確的情況下才允許使用者設定新密碼。這樣的設計減少了因為外部程式碼錯誤使用(如直接設定密碼而不進行舊密碼驗證)導致的安全問題,同時也確保了類內部資料的完整性和安全性。

  • 提高複用性:封裝後的物件可以作為一個黑盒被重複使用,無需關心物件內部的複雜邏輯
  1. 封裝後的物件作為一個黑盒被重複使用體現在:
UserCredentials adminCredentials = new UserCredentials("admin", "adminPass");
UserCredentials userCredentials = new UserCredentials("user", "userPass");

// 在不同場景中重複使用物件:
if (adminCredentials.authenticate("adminPass")) {
    // 執行管理員操作
}

if (userCredentials.authenticate("userPass")) {
    // 執行使用者操作
}

adminCredentialsuserCredentialsUserCredentials 的例項,在建立它們之後可以多次使用其 authenticate 方法來驗證密碼,這裡的例項就像是提供認證功能的黑盒,使用者不必關心裡面的邏輯是怎樣的。

  1. 無需關心物件內部的複雜邏輯體現在
private String username;
private String password;

private void setPassword(String password) {
    this.password = password;
}

由於 usernamepassword 屬性被宣告為私有的,外部程式碼不能直接訪問或修改它們。設定密碼的邏輯被隱藏在 setPassword 方法中,而這個方法也是私有的。外部程式碼需要透過公有方法如建構函式或 resetPassword 這些公有介面進行操作,因此外部程式碼不必關心如何儲存或驗證密碼的內部邏輯,只需呼叫這些公有方法即可實現功能。

  • 易於維護:封裝的程式碼更易理解與修改,修改內部實現時不會影響到使用該物件的程式碼
  1. 封裝的程式碼更易理解與修改體現在
public boolean authenticate(String inputPassword) {
    return inputPassword != null && inputPassword.equals(this.password);
}

public void resetPassword(String oldPassword, String newPassword) {
    if (authenticate(oldPassword)) {
        this.password = newPassword;
        System.out.println("密碼重置成功。");
    } else {
        System.out.println("舊密碼不正確,密碼重置失敗。");
    }
}

authenticateresetPassword 這兩個公有方法中,封裝的程式碼很易於理解:一個用於驗證密碼,一個用於重新設定密碼。如果我們需要修改密碼的儲存邏輯,只需修改這些方法的內部邏輯,而無需修改方法的簽名或其他使用這些方法的程式碼。

  1. 修改內部實現時不會影響到使用該物件的程式碼體現在
private String username;
private String password;

因為 usernamepassword 是私有屬性,所以它們對外部程式碼是不可見和不可訪問的。我們可以在不改變任何使用 UserCredentials 物件的程式碼的情況下,自由改變這些屬性的內部表示方法(比如對密碼進行加密儲存)。因為任何這樣的改變都會被 UserCredentials 類的公共介面所封裝和抽象化,從而不會洩露出去或者影響到依賴於這些公共介面的程式碼。

  • 介面與實現分離:提供清晰的介面,使得物件之間的依賴關係只基於介面,降低了耦合度
  1. 提供清晰的介面體現在
public boolean authenticate(String inputPassword);
public void resetPassword(String oldPassword, String newPassword);
public String getUsername();

這些公共方法形成了 UserCredentials 類的介面,它為外部程式碼提供了清晰的通訊協議,明確了可以進行的操作。使用這個類的程式碼只需要知道這些方法的宣告和預期行為,不需要了解它們背後的具體實現。

  1. 使得物件之間的依賴關係只基於介面體現在
UserCredentials credentials = new UserCredentials("username", "password");
boolean valid = credentials.authenticate("password");

只要 authenticate 方法的介面保持不變,外部程式碼就可以正常工作,完全無須關心 UserCredentials 內部是如何處理認證邏輯的。

  1. 降低了耦合度體現在
private void setPassword(String password) {
    // 假設這裡改用了一種新的加密方式來設定密碼
    this.password = encryptPassword(password);
}

即使改變了 setPassword 方法的內部實現(如加密),由於這個方法是私有的,外部程式碼不會受到影響。這種隔離提高了系統的模組化,使得各個部分可以獨立變化而不互相干擾,從而降低了耦合度。

  • 隱藏實現細節,簡化介面:使用者只需知道物件公開的方法,不必瞭解其內部的複雜過程

應用場景

封裝的應用場景:

  • 類的設計:在類定義時,通常將屬性私有化(private),透過公共的方法(public methods)來訪問和修改這些屬性
  • 模組化元件:在設計模組化的系統時,每個元件都透過封裝來定義自己的行為和介面,使得系統更易於組合和擴充套件
  • 庫和框架的開發:開發者提供庫和框架時,會透過封裝隱藏複雜邏輯,只暴露簡潔的 API 介面給其他開發者使用
  • 隔離變化:將可能變化的部分封裝起來,變化發生時,只需修改封裝層內部,不影響外部使用

透過封裝,能夠構建出結構清晰、易於管理和維護的程式碼。

完整程式碼可在此查閱:GitHub

繼承(Inheritance)

繼承是一種能夠讓新建立的類(子類或派生類)接收另一個類(父類或基類)的屬性和方法的機制。

在 Java 中,繼承是透過使用 extends 關鍵字來實現的。從理論上解釋一下,然後再透過程式碼示例來加深理解。

IS-A 關係

IS-A 是一種表達類之間關係的方式,主要用來表明一個實體(子類)是另一個實體(父類)的一種特殊型別。例如,Cat(貓)是 Animal(動物)的一種特殊型別。因此,可以說 Cat IS-A Animal。

里氏替換原則(Liskov Substitution Principle)

這是一個物件導向設計的原則,它表明如果 S 是 T 的一個子型別(在 Java 中意味著 S 類繼承自 T 類),那麼任何期望 T 類的物件的地方都可以用 S 類的物件來替換,而不會影響程式的行為。

向上轉型(Upcasting)

向上轉型是指子類型別的引用自動轉換成父類型別。向上轉型在多型中是常見的,它允許將子類的物件賦值給父類的引用。例如,可以將 Cat 型別的物件賦值給 Animal 型別的引用。

以程式碼形式展示上述概念:

/**
 * 動物
 *
 * @author BNTang
 * @date 2024/03/10 09:36:41
 * @description 建立一個表示動物的基類(父類)
 */
class Animal {
    // 動物類有一個叫的方法
    public void makeSound() {
        System.out.println("動物發出聲音");
    }
}

// 建立一個 Cat 類(子類),繼承自 Animal 類
class Cat extends Animal {
    // 重寫父類的 makeSound 方法
    @Override
    public void makeSound() {
        // 這裡的呼叫體現了多型性,即 Cat 的叫聲不同於一般 Animal
        System.out.println("貓咪喵喵叫");
    }
}

public class InheritanceExample {
    public static void main(String[] args) {
        // Upcasting: 將 Cat 物件向上轉型為 Animal 型別
        Animal myAnimal = new Cat();
        // 雖然 myAnimal 在編譯時是 Animal 型別,但實際執行的是 Cat 的 makeSound 方法
        myAnimal.makeSound();

        // 建立一個 Animal 型別的物件,呼叫 makeSound 方法
        Animal anotherAnimal = new Animal();
        anotherAnimal.makeSound();

        // 這裡可以看到,Cat 物件(myAnimal)能夠替換 Animal 物件(anotherAnimal)的位置,
        // 並且程式的行為沒有發生錯誤,體現了里氏替換原則
    }
}

定義了兩個類:Animal 和 Cat。

Cat 類繼承自 Animal 類,並重寫了 makeSound 方法。在 main 方法中,建立了一個 Cat 物件,並將其向上轉型為 Animal 型別的引用 myAnimal。呼叫 myAnimal 的 makeSound 方法時,會執行 Cat 類的重寫方法而不是 Animal 類的方法,這就體現了多型性和里氏替換原則。同時,Cat 物件(向上轉型後的 myAnimal)可以在任何需要 Animal 物件的地方使用,這也滿足了 IS-A 關係的定義

完整程式碼可在此查閱:GitHub

多型(Polymorphism)

多型可以允許使用一個統一的介面來操作不同的底層資料型別或物件。多型分為 編譯時 多型和 執行時 多型兩種型別。
編譯時多型(方法的過載),也被稱為靜態多型,主要是透過 方法過載(Method Overloading)來實現的。方法過載指的是在同一個類中存在多個同名的方法,但這些方法的引數列表不同(引數數量或型別不同)。

編譯器根據方法被呼叫時傳入的引數型別和數量,來決定具體呼叫哪個方法。這種決策是在編譯時做出的,因此稱為編譯時多型。

方法過載

程式碼示例:

/**
 * 印表機類
 * 用於演示方法過載
 *
 * @author BNTang
 */
class Printer {
    /**
     * 列印字串
     *
     * @param content 要列印的字串
     */
    public void print(String content) {
        System.out.println("列印字串: " + content);
    }

    /**
     * 過載 print 方法,引數型別為 int,與列印字串的方法區分開來
     *
     * @param number 要列印的數字
     */
    public void print(int number) {
        System.out.println("列印數字: " + number);
    }
}

public class OverloadingExample {
    public static void main(String[] args) {
        Printer printer = new Printer();

        // 呼叫 print 方法列印字串
        printer.print("Hello, World!");

        // 呼叫過載的 print 方法列印數字
        printer.print(12345);

        // 編譯器根據引數型別來決定呼叫哪個方法
    }
}

執行時多型,也被稱為動態多型或動態繫結,是透過 方法覆蓋(Method Overriding)實現的。

執行時多型是在繼承的基礎上工作的,所以只要其中子類覆蓋父類的方法。

執行時多型的決策是在程式執行期間進行的,即虛擬機器在執行時刻根據物件的實際型別來確定呼叫哪個類中的方法。

方法覆蓋

程式碼示例:

/**
 * 動物
 * 建立一個表示動物的基類(父類)
 *
 * @author BNTang
 */
class Animal {
    public void makeSound() {
        System.out.println("動物發出聲音");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        // 向上轉型
        Animal animal = new Dog();

        // 執行時多型,呼叫的是 Dog 類的 makeSound 方法
        animal.makeSound();
    }
}

雖然在編譯時 animal 的型別是 Animal,但是在執行時 JVM 會呼叫實際物件型別(也就是 Dog)的 makeSound 方法,因此輸出的將是 "汪汪汪",而不是 "動物發出聲音"。這就是執行時多型的體現。

執行時多型的三個條件

  1. 繼承:子類需要繼承父類
  2. 方法覆蓋:子類需要提供一個具體的實現,這個實現覆蓋了父類的方法
  3. 向上轉型:你可以將子類型別的引用轉換為父類型別的引用(即將子類物件賦值給父類引用),之後透過這個父類引用來呼叫方法時,執行的將是子類的覆蓋實現

利用多型寫出可擴充套件性和可維護性更佳的程式碼,能夠應對不斷變化的需求。使得可以透過相同的介面來呼叫不同類的實現,提供了軟體設計的靈活性。

完整程式碼可在此查閱:GitHub

抽象(Abstraction)

相關文章