[Java] OOP 概論
- 內聚性 (Cohesion):模組本身的功能強度,意即模組的完整性。
- 耦合性 (Coupling):模組與模組之間的依賴關係,意即模組之間的關聯性。
以 OOP、甚至應該說多數程式設計的角度來看,高內聚性與低耦合性是我程式設計時所追求的目標。
但這之間必須要拿捏有度,不可過分偏激。
舉例來說,如果極高度內聚性,但耦合性趨近於無,那這跟在同一支 file 上寫完所有 code 無異,這樣就失去模組化的意義了。
所以高內聚、低耦合是相對且主觀的概念,必須視情況而定。
OOP (Object-Oriented)
OOP 的核心組成基本是以類別 (class) 為基礎。
以 class 做為藍圖 (Blueprint),來建立具有特徵與行為的物件 (Object)。
public class practice {
public static void main(String[] args) {
Student obj1 = new Student("王大明", "S123", "DigitalMedia", "S123", 90, 80, 60);
Student obj2 = new Student("李大一", "D456", "ComputerScience", "D456", 20, 40, 70);
obj1.printStudentInfo();
obj1.printStudentScores();
System.out.println();
obj2.printStudentInfo();
obj2.printStudentScores();
}
}
class Student {
public String name;
public String StudentId;
public String department;
public String id;
public float chScore;
public float enScore;
public float mathScore;
public Student(String name, String StudentId, String department, String id, float chScore, float enScore, float mathScore) {
this.name = name;
this.StudentId = StudentId;
this.department = department;
this.id = id;
this.chScore = chScore;
this.enScore = enScore;
this.mathScore = mathScore;
}
public void printStudentInfo () {
System.out.println("Student name: " + this.name);
System.out.println("Student ID: " + this.StudentId);
System.out.println("Student department: " + this.department);
System.out.println("Personal ID: " + this.id);
}
public void printStudentScores () {
System.out.println("Chinese score: " + this.chScore);
System.out.println("English score: " + this.enScore);
System.out.println("Math score: " + this.mathScore);
isGraduate();
}
private void isGraduate () {
float total = chScore + enScore + mathScore;
float avg = total / 3;
if (avg > 60) {
System.out.println("Pass! Your semester scores is: " + avg);
} else {
System.out.println("Not pass! Your semester scores is: " + avg);
}
}
}
OOP 三大特性
- 封裝 (Encapsulation):將物件區分為可被外界使用的特性以及受保護的內部特性。因此封裝有助於維護物件的完整性與安全性 (ex: 資訊的隱藏)。
- 繼承 (Inheritance):一種可以避免重複定義相同屬性與方法的概念。 → 但 child class 依舊可以擁有自己特有的屬性與方法。
- 多型 (Polymorphism):允許物件以不同的形式出現,意即允許物件有同一行為,但有不同的結果。主要透過方法覆寫 (Method Overriding) 與方法重載 (Method Overloading) 來實現多型。 → 簡單來說就是可以利用 parent class 的 method,但 child class 執行出來卻是另一結果。
封裝 (Encapsulation)
如前面所述,OOP 的封裝是一種將變數 & 方法等做內外部區分的概念,口語化一點來說有點像是隱私權的概念在程式裡的實踐。
那在 Java 中,封裝主要是透過存取修飾詞 (Access Modifiers) 來達成的。
所謂的存取修飾詞,簡單來說就是用來定義變數或方法的可見範圍,一共有四種修飾詞:
- public:公開的,表示該變數或方法可以被任何其他類別存取。
- protected:受保護的,表示該變數或方法只能被同一個 package 中的類別或其子類別存取。
- default (無修飾詞):預設的,表示該變數或方法只能被同一個 package 中的類別存取。
- private:私有的,表示該變數或方法只能在該類別內部存取。
沒有很懂 Java 的 package 範疇的話,可以先直白一點記每個寫在同一 folder 下的 class 都是同一 package 就好。
所以最容易搞混的是 default 與 protected,但他們差異是在於 protected 允許子類別存取,而子類別是可以不跟父類別寫在同一 package 下的。
所以嚴格來說,封裝達到了起碼三件事情:
- 安全性:透過限制變數與方法的可見範圍,防止外部直接存取或修改物件的內部狀態,確保資料的完整性與安全性。
- 複用性:透過封裝,可以將物件的內部實現細節隱藏起來,使用者只要會調用公開的方法即可,這樣可以提高重複邏輯的複用性。
- 易維護性:封裝使得物件的內部實現細節與外部使用者隔離,所以在調整或修改物件的內部實現時,只要能確保公開的方法或變數不變,外部使用者就不需要做任何修改,這樣可以提高程式碼的易維護性。
繼承 (Inheritance)
既然叫做繼承,那就一定有個親緣關係存在。
OOP 的世界裡,其實不需要每個 class 都要開發者詳細定義內部的屬性與方法,只要是有相似性的 class,就可以把它們相似的部分抽出來定義在一個父類別 (Parent Class,或稱基礎類別) 裡,然後其他子類別 (Child Class,或稱衍生類別) 就可以透過繼承這個父類別的屬性與方法,來避免重複定義相同的屬性與方法。
口語一點來說,舉個生物的例子。
Parent class 是哺乳類 (Mammal),而 Child class 則是狗 (Dog)、貓 (Cat)、人類 (Human) 等等。
哺乳類這個 parent class 裡面會定義一些所有哺乳類都會有的屬性與方法 (ex: 有毛髮、胎生、會哺乳),而狗、貓、人類這些 child class 就可以繼承這些屬性與方法,然後再各自定義自己特有的屬性與方法 (ex: 狗會吠叫、貓會抓老鼠、人類會寫 code www)。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Fighter fighter = new Fighter();
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入戰鬥機物件 fighter 之相關資訊:");
// 範例輸入:
// 洛克希德馬丁 F-16 Chinese-1 A0001 1 3986 像鯊魚 20mm火神炮 麻雀飛彈 127mm火箭
String line = scanner.nextLine();
scanner.close();
String[] parts = line.split(" ");
fighter.setManufacturer(parts[0]);
fighter.setFlightVehicleType(parts[1]);
fighter.setFlightVehicleId(parts[2]);
fighter.setEngineId(parts[3]);
fighter.setPilotAmount(Integer.parseInt(parts[4]));
fighter.setFuelContent(Double.parseDouble(parts[5]));
fighter.setFlightVehicleDesc(parts[6]);
fighter.setWeapon(parts[7], parts[8], parts[9]);
fighter.printAirplane();
}
}
/* =========================
父類別:飛行載具
========================= */
class FlightVehicle {
protected String flightVehicleDesc;
public void setFlightVehicleDesc(String desc) {
this.flightVehicleDesc = desc;
}
}
/* =========================
中介父類別:飛機
========================= */
class Airplane extends FlightVehicle {
protected String manufacturer;
protected String flightVehicleType;
protected String flightVehicleId;
protected int pilotAmount;
protected double fuelContent;
private String engineId;
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public void setFlightVehicleType(String type) {
this.flightVehicleType = type;
}
public void setFlightVehicleId(String id) {
this.flightVehicleId = id;
}
public void setPilotAmount(int pilotAmount) {
if (pilotAmount > 0) {
this.pilotAmount = pilotAmount;
}
}
public void setFuelContent(double fuelContent) {
if (fuelContent >= 0) {
this.fuelContent = fuelContent;
}
}
public void setEngineId(String engineId) {
this.engineId = engineId;
}
public void printAirplane() {
System.out.println("製造商: " + manufacturer);
System.out.println("飛機型號: " + flightVehicleType);
System.out.println("飛機編號: " + flightVehicleId);
System.out.println("引擎號碼: " + engineId);
System.out.println("飛行員人數: " + pilotAmount);
System.out.println("油箱容量 (L): " + fuelContent);
System.out.println("飛機外觀: " + flightVehicleDesc);
}
}
/* =========================
子類別:戰鬥機
========================= */
class Fighter extends Airplane {
private String gunName;
private String missileName;
private String rocketName;
public void setWeapon(String gunName, String missileName, String rocketName) {
this.gunName = gunName;
this.missileName = missileName;
this.rocketName = rocketName;
}
@Override
public void printAirplane() {
super.printAirplane();
System.out.println("機槍名稱: " + gunName);
System.out.println("飛彈名稱: " + missileName);
System.out.println("火箭名稱: " + rocketName);
}
}
一個沒那麼重要的分類:
- 單一繼承 (Single Inheritance):一個子類別只能繼承一個父類別。但一個父類別可以有多個子類別繼承它。
- 多層繼承 (Multilevel Inheritance):子類別可以繼承另一個子類別,形成多層繼承結構。例如,A 繼承 B,B 繼承 C。
但繼承也不是什麼東西都可以繼承的。
前面封裝有寫到如 private 這類只能給予 class 內部存取的變數與方法,就是無法被 child class 繼承到的。
這種時候如果子類別需要使用父類別的這些 private 變數與方法,就必須要透過父類別提供的 public 或 protected 方法來間接存取。
父類別的建構子也是無法被子類別直接繼承使用的,但子類別可以透過在自己的建構子裡呼叫 super() 來間接使用父類別的建構子,這點下個章節會寫到。
這裡要注意,如果類別內沒手動定義「無參數」的建構子,只定義了「含參數」的建構子,這寫法在當這個 class 沒任何子輩關係時是沒問題的。
但若是要做為父類別,因為子類別實體化時會預設呼叫父類別的無參數建構子,這時就會因為找不到無參數建構子而報錯。
所以撰寫 Java 的良好習慣是,不管有沒有子類別,都建議手動定義一個無參數建構子,以避免這種問題發生。
在多層繼承的情況下,在這裡假設有三層繼承關係:GrandParent → Parent → Child。
當 Child 類別的建構子被呼叫時,會依序呼叫 Parent 類別的建構子,然後再呼叫 GrandParent 類別的建構子,最後才會執行 Child 類別的建構子內容。
這是因為在物件實體化的過程中,必須先初始化父類別的屬性與方法,然後才能初始化子類別的屬性與方法。
super、this & final 關鍵字
這三個關鍵字在繼承的概念中是很重要的,而且時常會出現:
super: 指向「父類別的實例」。super():用來呼叫父類別的建構子 (Constructor),通常用在子類別的建構子中,以初始化父類別的屬性。super.屬性名稱或super.方法名稱():用來存取父類別的屬性或方法,特別是在子類別中有同名屬性或方法時,可以用super來區分。
this: 指向「目前類別的實例」。this():用來呼叫目前類別的其他建構子,通常用在建構子中,以避免重複程式碼。this.屬性名稱或this.方法名稱():用來存取目前類別的屬性或方法,特別是在方法參數與屬性同名時,可以用this來區分。
final: 用來宣告「不可變更」的變數、方法或類別。final變數:一旦被賦值後,值就不能再被更改。final方法:不能被子類別覆寫 (Override)。final類別:不能被繼承。
public class Main {
public static void main(String[] args) {
Child child = new Child("Dragon", "Jeremy");
child.printName();
}
}
/* =========================
父類別
========================= */
class Father {
protected final String fatherName;
public Father(String fatherName) {
this.fatherName = fatherName;
}
public final void printFatherName() {
System.out.println("My father's name is: " + fatherName);
}
}
/* =========================
子類別
========================= */
class Child extends Father {
private String name;
public Child(String fatherName, String name) {
super(fatherName); // 呼叫父類別建構子
this.name = name; // this 指向子類別自己的欄位
}
public void printName() {
System.out.println("My name is: " + this.name);
super.printFatherName(); // 呼叫父類別的方法
}
}
多型 (Polymorphism)
前面講了,多型的概念是允許物件以不同的形式出現,意即允許物件有同一行為,但有不同的結果。
要實現多型,有兩個方法:
- 方法覆寫 (Method Overriding):即子類別繼承父類別的方法,並重新定義該方法的行為。
- 方法多載 (Method Overloading):即在同一個類別中,定義多個同名但參數不同 (參數型態或數量不同) 的方法。
方法覆寫 (Method Overriding)
Overriding 基本有四個指標,四個條件都要符合才算是方法覆寫:
- return type 必須相同。
- 參數個數與型態必須相同。
- 方法名稱必須相同。
- 內容必須不同。
另外,子類別覆寫父類別的方法時,存取修飾詞的權限不能比父類別的方法更嚴格 (ex: 父類別是 public,子類別就不能是 protected 或 private)。
並且 final、static、private 跟建構子都是不能被覆寫的。
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.speak();
}
}
class Animal {
public void speak() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Dog barks");
}
}
方法多載 (Method Overloading)
前一篇在講 Java 語法的文章亦提到過多載,但這裡還是提出重點說明多載有三項條件,都必須符合才算是方法多載:
- 方法名稱必須相同。
- 參數個數、型態或順序必須不同。
- 回傳型別的不同不能單獨作為判斷依據,必須配合參數差異
public class Calculator {
// 加法:兩個 int
int add(int a, int b) {
return a + b;
}
// 加法:三個 int
int add(int a, int b, int c) {
return a + b + c;
}
// 加法:兩個 double
double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // 使用 int, int
System.out.println(calc.add(2, 3, 4)); // 使用 int, int, int
System.out.println(calc.add(2.5, 3.5)); // 使用 double, double
}
}
Summary
這裡簡單記錄一項結合封裝、繼承與多型的範例。
public class Main {
public static void main(String[] args) {
// 多型:付款方式都是 Payment,但實際行為不同
Payment creditCardPayment = new CreditCardPayment("Jeremy");
Payment linePayPayment = new LinePayPayment("Jeremy");
creditCardPayment.pay(1000); // 信用卡付款
linePayPayment.pay(1000); // Line Pay 付款
// 多載:同一功能,不同參數
creditCardPayment.pay(1000, "USD");
linePayPayment.pay(500, "TWD");
}
}
/* =====================
父類別:付款(抽象概念)
===================== */
abstract class Payment {
// 封裝:使用者資料不允許外部直接修改
private String userName;
public Payment(String userName) {
this.userName = userName;
}
protected String getUserName() {
return userName;
}
// 多型:由子類別決定實際付款行為
public abstract void pay(int amount);
// 多載:指定幣別的付款方式
public void pay(int amount, String currency) {
System.out.println(
userName + " pays " + amount + " " + currency
);
}
}
/* =====================
子類別:信用卡付款
===================== */
class CreditCardPayment extends Payment {
public CreditCardPayment(String userName) {
super(userName);
}
@Override
public void pay(int amount) {
System.out.println(
getUserName() + " pays " + amount + " by Credit Card"
);
}
}
/* =====================
子類別:Line Pay 付款
===================== */
class LinePayPayment extends Payment {
public LinePayPayment(String userName) {
super(userName);
}
@Override
public void pay(int amount) {
System.out.println(
getUserName() + " pays " + amount + " by Line Pay"
);
}
}