Java中的面向对象编程:类、对象、封装、继承和抽象

Java 一直是全球三大最受歡迎的編程語言之一。在企業軟件開發、Android 移動應用程式及大規模網頁應用方面的應用無人能及。它強大的類型系統、廣泛的生態系統以及「一次編寫,隨處運行」的能力,使其特別適合用於構建穩健、可擴展的系統。在本文中,我們將探討 Java 的面向對象編程特性如何使開發者能有效利用這些能力,通過適當的代碼組織和重用來構建可維護且可擴展的應用程式。

有關組織和運行 Java 代碼的注意事項

在我們開始編寫任何代碼之前,讓我們做一些設置。

與其語法一樣,Java 在代碼組織方面有著嚴格的規則。

首先,每個公共類必須放在自己的文件中,文件名與類名完全相同,但要使用.java擴展名。因此,如果我想寫一個Laptop類,文件名必須是Laptop.java區分大小寫。您可以在同一文件中擁有非公共類,但最好將它們分開。我知道我們正在超前地討論在編寫代碼之前組織類的問題,但事先大致了解應將事物放在何處是一個好主意。

所有 Java 專案必須包含一個 Main.java 檔案,裡面有 Main 類別。這是你透過創建物件來測試你的類別的地方。

要運行 Java 代碼,我們將使用 IntelliJ IDEA,這是一個流行的 Java IDE。安裝 IntelliJ 之後:

  1. 創建一個新的 Java 項目(檔案 > 新建 > 項目)
  2. 右鍵點擊 src 資料夾以創建 Main.java 檔案,並貼上以下內容:
public class Main { public static void main(String[] args) { // 在此處創建並測試物件 } }

每當我們談到類別時,我們會在其他檔案中編寫程式碼,而不是Main.java檔案。但是,如果我們要談論建立和測試物件,我們會切換到Main.java

要執行程式,您可以點擊主方法旁邊的綠色播放按鈕:

輸出將顯示在底部的執行工具窗口中。

如果您對 Java 完全陌生,請查看我們的 Java 入門課程,該課程涵蓋了 Java 數據類型和控制流程的基本概念,然後再繼續。

否則,我們就直接開始吧。

Java 類和對象

那麼,什麼是類別呢?

類別是 Java 中用來表示現實世界概念的程式設計結構。例如,考慮這個 MenuItem 類別(在你的 IDE 中建立一個檔案來撰寫這個類別):

public class MenuItem { public String name; public double price; }

這個類別提供了藍圖或範本,以表示餐廳中的各種菜單項目。通過更改類別的兩個屬性,name,price, 我們可以創建無數的菜單物件,例如漢堡或沙拉。

因此,在Java中創建類別,您需要開始一行描述類別的訪問級別(privatepublicprotected),然後是類別名稱。大括號之後,您立即概述類別的屬性。

但是我們如何創建屬於這個類別的物件呢?Java 通過構造方法來實現這一點:

public class MenuItem { public String name; public double price; // 構造方法 public MenuItem(String name, double price) { this.name = name; this.price = price; } }

構造方法是一個特殊的方法,在我們從類中創建新物件時被調用。它使用我們提供的值來初始化物件的屬性。在上面的例子中,構造方法接受一個名稱和價格參數,並使用“this”關鍵字將它們分配給物件的字段,以便引用未來的物件實例。

建構函數的語法與其他類方法不同,因為它不需要您指定返回類型。此外,建構函數必須與類的名稱相同,並且應該擁有與您在類定義後聲明的屬性數量相同的屬性。在上面,建構函數創建了兩個屬性,因為我們在類定義後聲明了兩個:nameprice

在您編寫完類及其建構函數後,可以在主方法中創建該類的實例(對象):

public class Main { public static void main(String[] args) { // 在這裡創建對象 MenuItem burger = new MenuItem("Burger", 3.5); MenuItem salad = new MenuItem("Salad", 2.5); System.out.println(burger.name + ", " + burger.price); } }

輸出:

Burger, 3.5

在這裡,我們將創建兩個MenuItem對象分別賦值給變量burgersalad。在Java中,根據需要,必須聲明變量的類型,即MenuItem。接著,要創建我們類的一個實例,我們寫入new關鍵字,然後調用構造方法。

除了构造函数之外,您可以创建常规方法为您的类添加行为。例如,下面,我们添加一个方法来计算税后总价:

public class MenuItem { public String name; public double price; // 构造函数 public MenuItem(String name, double price) { this.name = name; this.price = price; } // 用于计算税后价格的方法 public double getPriceAfterTax() { double taxRate = 0.08; // 8% 的税率 return price + (price * taxRate); } }

现在我们可以计算包括税在内的总价:

public class Main { public static void main(String[] args) { MenuItem burger = new MenuItem("Burger", 3.5); System.out.println("Price after tax: $" + burger.getPriceAfterTax()); } }

输出:

Price after tax: $3.78

封装

类的目的是提供创建对象的蓝图。然后其他脚本或程序将使用这些对象。例如,我们的MenuItem 对象可以被用户界面使用,以在屏幕上显示它们的名称、价格和图片。

因此,我們必須設計我們的類別,使它們的實例只能按照我們的意圖使用。現在,我們的MenuItem 類別非常基本且容易出錯。一個人可能會創建具有荒謬屬性的對象,比如定價為負數的蘋果派或百萬美元的三明治:

// 在 Main.java 內 MenuItem applePie = new MenuItem("Apple Pie", -5.99); // 負數價格! MenuItem sandwich = new MenuItem("Sandwich", 1000000); // 不合理昂貴 System.out.println("Apple pie price: $" + applePie.price); System.out.println("Sandwich price: $" + sandwich.price);

因此,在編寫類別後的首要任務是通過限制其屬性的方式來保護它們的創建和訪問。首先,我們希望僅允許對價格設置正值,並設定一個最大值,以避免意外顯示荒唐昂貴的物品。

Java通過使用設置器方法來實現這一目標:

public class MenuItem { private String name; private double price; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { this.name = name; setPrice(price); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

讓我們來看看上面代碼塊中的新內容:

1. 通過添加 private 關鍵字使屬性私有化。這意味著它們只能在 MenuItem 類中被訪問。封裝從這一關鍵步驟開始。

2. 我們添加了一個新的常數 MAX_PRICE,即:

  • private(僅在類內部可訪問)
  • 靜態(在所有實例之間共享)
  • 最終(初始化後無法更改)
  • 設置為 $100.0 作為合理的最高價格

3. 我們新增了一個 setPrice() 方法,它:

  • 接收一個價格參數
  • 驗證價格不是負數
  • 驗證價格不超過 MAX_PRICE
  • 如果驗證失敗,則拋出帶有描述性消息的 IllegalArgumentException
  • 僅在所有驗證通過的情況下設置價格

4. 我們修改了構造函數以使用 setPrice() 而不是直接賦值價格。這確保了在對象創建期間進行價格驗證。

我們剛實現了良好面向對象設計的核心支柱之一——封裝。這種範式強制實施數據隱藏和對象屬性的受控訪問,確保內部實現細節受到外部干預的保護,並且只能通過明確定義的接口進行修改。

通過將封裝應用於名字屬性來加深印象。想像一下我們有一家只供應拿铁、卡布奇诺、濃縮咖啡、美式咖啡和摩卡的咖啡店。

因此,我們的菜單項目名稱只能是這個列表中的一項。以下是我們如何在代碼中強制執行這一點:

// 這裡是類的其餘部分 ... private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private String name; public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); }

上面的代碼實現了咖啡店菜單項目的名稱驗證。讓我們來分析一下:

1. 首先,它定義了一個私有的靜態最終數組 VALID_NAMES,其中包含唯一允許的飲品名稱:拿鐵、卡布奇諾、濃縮咖啡、美式咖啡和摩卡。這個數組的特點是:

  • 私有:僅在類內部可訪問
  • 靜態:在所有實例之間共享
  • final:初始化後無法修改

2. 宣告私有的 String name 欄位來儲存飲料名稱

3. setName() 方法實現驗證邏輯:

  • 接受一個 String name 參數
  • 將其轉換為小寫以進行不區分大小寫的比較
  • 遍歷 VALID_NAMES 陣列
  • 如果找到匹配项,设置名称并返回
  • 如果找不到匹配项,则抛出一个带有描述性消息的IllegalArgumentException,列出所有有效选项

到目前为止这是完整的类:

public class MenuItem { private String name; private double price; private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { setName(name); setPrice(price); } public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

在我们保护属性创建的方式之后,我们还想保护它们的访问方式。这是通过使用getter方法来完成的:

public class MenuItem { // 其余代码在这里 ... public String getName() { return name; } public double getPrice() { return price; } }

Getter方法提供对类的私有属性的受控访问。它们解决了直接属性访问可能导致不希望的修改并破坏封装的问题。

例如,如果沒有getter,我們可能會直接訪問屬性:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.name; // 直接訪問屬性 item.name = "INVALID"; // 可以直接修改,繞過驗證

有了getter,我們強制執行正確的訪問:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.getName(); // 通過getter進行受控訪問 // item.name = "INVALID"; // 不允許 - 必須使用setName()進行驗證

這種封裝:

  1. 通過防止無效修改來保護數據完整性
  2. 允許我們更改內部實現而不影響使用該類的代碼
  3. 提供一個可以包含額外邏輯的單一訪問點(如有需要)
  4. 使代碼更易於維護且不易出錯

繼承

我們的類別開始看起來不錯,但仍然存在許多問題。例如,對於一家提供多種菜餚和飲品的大型餐廳,這個類別的靈活性不足。

如果我們想添加不同類型的食品項目,將面臨幾個挑戰。一些菜餚可以準備外帶,而另一些則需要立即食用。菜單項目的價格和折扣可能有所不同。菜餚可能需要溫度追蹤或特殊儲存。飲品可以是熱的或冷的,並且可以自定義配料。項目可能需要過敏原信息和份量選項。目前的系統無法處理這些不同的要求。

繼承為所有這些問題提供了一個優雅的解決方案。它允許我們通過定義一個基本的MenuItem類來創建菜單項的專業版本,然後創建繼承這些基本功能但添加獨特功能的子類。

例如,我們可以為帶有溫度選項的飲料創建一個Drink類,為需要立即消費的物品創建一個Food類,並為具有特殊存儲需求的物品創建一個Dessert類 — 所有這些都繼承核心菜單項功能。

擴展類

讓我們從實現這些想法開始 Drink

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { this.name = name; this.price = price; this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

要定義一個繼承自父類的子類,我們在子類名稱後使用 extends 關鍵字,然後跟上父類的名稱。在類的定義之後,我們定義這個子類擁有的任何新屬性並實現它的構造函數。

但请注意,我们需要重复初始化 nameprice 以及 isCold。这并不理想,因为父类可能有数百个属性。此外,上面的代码在编译时会抛出错误,因为这不是初始化父类属性的正确方式。正确的方式是使用 super 关键字:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

super 關鍵字是用來呼叫父類別的建構子。在這個案例中,super(name, price) 呼叫 MenuItem 的建構子來初始化這些屬性,避免程式碼重複。我們只需要初始化新的 isCold 屬性,這是 Drink 類別特有的。

關鍵字非常靈活,因為您可以在子類的任何部分中使用它來引用父類。例如,要調用父方法,您可以使用super.methodName(),而super.attributeName則是用於屬性。

方法覆蓋

現在,假設我們想要在我們的類中添加一個新方法來計算含稅總價。由於不同的菜單項目可能有不同的稅率(例如,現成食品與包裝飲料),我們可以使用方法覆蓋在每個子類中實現特定的稅收計算,同時在父類中保持共同的方法名稱。

這看起來是這樣的:

public class MenuItem { // 菜單項類別的其餘部分 public double calculateTotalPrice() { // 預設稅率為10% return price * 1.10; } } public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { // 食物的稅率為15% return super.getPrice() * 1.15; } } public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } @Override public double calculateTotalPrice() { // 飲料的稅率為8% return super.getPrice() * 1.08; } }

在這個例子中,方法重寫允許每個子類別提供其自己的實作 calculateTotalPrice()

該基礎 MenuItem 類別定義了 10% 的預設稅率計算。

食品飲料 類別擴展 菜單項目 時,它們將覆蓋此方法以實現自己的稅率:

  • 食品項目有較高的15%稅率
  • 飲料稅率較低,為8%

@Override註釋用於明確指示這些方法正在覆寫父類方法。這有助於捕捉錯誤,如果方法簽名與父類不匹配。

每個子類仍可以使用super.getPrice()來存取父類的價格,這展示了覆寫方法如何利用父類功能並添加自己的行為。

簡而言之,方法覆寫是繼承的重要部分,允許子類提供其在父類中定義的方法的實現,從而實現更具體的行為,同時保持相同的方法簽名。

抽象類

我們的MenuItem 類層次結構是有效的,但存在一個問題:是否應該允許任何人創建一個普通的MenuItem 對象?畢竟,在我們的餐廳中,每個菜單項目都是食物或飲料 – 不存在只是“通用菜單項目”的情況。

我們可以通過將MenuItem設置為抽象類來防止這種情況。抽象類僅提供基本藍圖 – 它只能用作繼承的父類,不能直接實例化。

要使 MenuItem 成為抽象的,我們在其訪問修飾符後添加 abstract 關鍵字:

public abstract class MenuItem { private String name; private double price; public MenuItem(String name, double price) { setName(name); setPrice(price); } // 現有的 getter/setter 保持不變 // 將此方法設為抽象——每個子類必須實現它 public abstract double calculateTotalPrice(); }

抽象類別也可以像上面的calculateTotalPrice()一樣擁有抽象方法。這些抽象方法作為合約,強制子類別提供它們的實現。換句話說,抽象類別中的任何抽象方法都必須由子類別實現。

所以,讓我們根據這些更改來重寫食物飲料

public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { return getPrice() * 1.15; // 15% 稅 } } public class Drink extends MenuItem { private boolean hasCaffeine; public Drink(String name, double price, boolean hasCaffeine) { super(name, price); this.hasCaffeine = hasCaffeine; } @Override public double calculateTotalPrice() { return getPrice() * 1.10; // 10% 稅 } }

通過這個菜單系統的實現,我們看到抽象和繼承如何共同創建靈活、易於維護的代碼,可以輕鬆適應不同的業務需求。

結論

今天,我們淺談了作為一個面向對象的編程語言 Java 的能力。我們涵蓋了類、對象的基礎知識,以及面向對象編程的幾個關鍵支柱:封裝、繼承和通過餐廳菜單系統實現的抽象。

要使這個系統適用於生產環境,你還有很多東西要學習,比如介面(抽象的一部分)、多態性和面向對象的設計模式。要了解更多有關這些概念的信息,請參考我們的 Java 面向對象編程介紹 課程。

如果你想測試自己對Java的知識,可以試著回答一些我們的Java面試問題文章中的問題。

Source:
https://www.datacamp.com/tutorial/oop-in-java