1. 程式人生 > >「MoreThanJava」Day 4:面向物件基礎

「MoreThanJava」Day 4:面向物件基礎

![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/7f300125-846f-4440-95f4-3fb397d7478e.png) - **「MoreThanJava」** 宣揚的是 **「學習,不止 CODE」**,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 **「幫助新朋友快速高質量的學習」**。 - 當然 **不論新老朋友** 我相信您都可以 **從中獲益**。如果覺得 **「不錯」** 的朋友,歡迎 **「關注 + 留言 + 分享」**,文末有完整的獲取連結,您的支援是我前進的最大的動力! # Part 1. 面向物件設計概述 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/5b68f16b-c095-4300-8bb4-4180760f9695.png) 面向物件程式設計 *(Object-Oriented Programming, OOP)* 是當今主流的程式設計範型,它取代了 `20` 世紀 `70` 年代的 "結構化" 或程序式程式設計技術。由於 Java 是面向物件的,所以必須熟悉 OOP 才能夠很好地使用 Java。 ## 瞭解抽象 **抽象的作用是將複雜的機制隱藏在一個物件中,僅保留我們與之互動所必須的資訊**。 為了說明這一點,我們可以想象平時使用 **「電梯」** 的場景。 ![熟悉的早晨等電梯!](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/bd4d9df6-9894-48cb-8db5-c6193bab8df8.png) 如果您在辦公樓工作,這可能是您日常工作的一部分。你按下向上或向下按鈕,然後等待門滑開。完成操作後,您進入一個 "盒子",該 "盒子" 的一面牆上有一個按鈕面板,然後按下所需的按鈕。當電梯到達您要去到的樓層後,您會擠過其他人然後走出去。 **要使用電梯,您只需要瞭解如何按下正確的按鈕就可以達到目的。** 而隱藏在電梯背後的支援它工作的一系列東西 —— 滑輪系統、機械、電線、減震器、安全系統等等... 您可以完全不知道也完全不必操心... ![我完全不知道他們在做什麼...](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/3d83e081-0ad5-4d2a-a569-48c441a78dbc.png) 電梯這個 "鐵盒子" 以及相應的按鈕面板,就是對整個「運輸系統」成功的抽象 *(事實上電梯背後還包含檢修、維護等一系列事情...)*,它隱藏了足夠多的細節,也極大地方便了我們的生活。 ## 什麼是物件 **簡單來說,物件是對現實世界的抽象。** *(例如上方對整個運輸系統抽象之後,就得到了「電梯」這個物件...)* 什麼東西是物件?什麼東西不是物件?這是一個困擾哲學家數千年的問題。勒內·笛卡爾 *(17世紀的哲學家)* 觀察到,人類是用面向物件的方式看待世界的 *(例如與電梯的互動)*。人類的大腦會從物件的角度認識世界 *(例如鳥類、魚類)*,我們的思想和記憶也被組織成物體和它們之間的關係 *(例如,鳥吃蟲)*。 ### 物件像是一種模板 亞里士多德大概是第一個深入研究 **型別** *(type)* 的哲學家,它曾經提出過 **魚類** 和 **鳥類** 這樣的概念。**所有的物件都是唯一的,但同時也是具有相同的特性和行為的物件所歸屬的類的一部分。** 這就好像我們拿著一個模具,我們可以使用該模具製作出各種各樣東西,每個東西都有自己的 "個性",但它們又都遵循一些相同的基本模式: ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/e03748fe-fd54-4200-9e79-b81f4b2df0b9.png) ### 物件的特徵 我們可以把你的「銀行賬戶」抽象成一個物件,但它不是由物質構成的。*(雖然您和銀行可以使用紙張和其他材料來記錄您的賬戶,但您的賬戶獨立於這些材料而存在。)* ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/4dceb90b-0dfc-4548-9d07-8fce72749155.png) 雖然它不是物質的,但你的賬戶是有 **屬性** 的 *(餘額、利率、持有者等..)*,**你可以對它做一些事情** *(存款、取款、檢視餘額等..,)*,**它自己也可以做一些事情** *(交易收費、積累利息等...)*。 這足夠清楚吧。事實上,這些特徵它們都有名字: - 物件具有 **標識 identity**;*(每個物件都是獨立的個體)* - 物件具有 **狀態 state**;*(它具有各種可能會改變的屬性)* - 物件具有 **行為 behavior**;*(它可以做事情,也可以讓別人對它做事情)* 這就是對一個物體的一般描述。*(上面的列表來自於 `1994` 年 `Grady Booch`/`Addison-Wesley` 出版的《面向物件分析與設計》一書。)* 當你開始編寫面向物件的軟體時,你會發現這個列表將幫助你決定你的物件應該是什麼樣。 ## 程式語言中的抽象過程 所有程式語言都提供抽象機制。**可以認為,人們所能夠解決的問題的複雜性直接取決於抽象的型別和質量**。 所謂的 "型別" 是指 "所抽象的是什麼?"。 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/efecfb0e-7614-4233-95e1-dbefa942f05f.png) 組合語言是對底層機器語言的輕微抽象,接著出現的許多 "命令式" 語言 *(如 FORTRAN、BASIC、C 等..)* 都是對組合語言的進一步抽象。 這些語言在組合語言基礎上有了很大幅度的改進,但是它們所作的主要抽象仍要求在解決問題時要基於計算機的結構,而不是基於所要解決的問題的結構來考慮。 > 傳統的結構化程式設計通過設計一系列的過程 *(即演算法)* 來求解問題。一旦確定了這些過程,就要開始考慮儲存資料的適當方式。 > > 這就是 `Pascal` 語言的設計者 `Niklaus Wirth` 將其著作命名為《演算法 + 資料結構 = 程式》*(Algorithms + Data Structures = Programs, Prentice Hall, `1975`)* 的原因。 > > 需要注意的是,在 `Wirth` 的這個書名中,演算法是第一位的,資料結構是第二位的,這就明確的表述了程式設計師的工作方式。首先要確定如何操作資料,然後再決定如何組織資料的結構,以便於操作資料。 > > 而 OOP 卻調換了這個次序,將資料放在第一位,然後再考慮操作資料的演算法。*(在 OOP 中,也有說法是:程式 = 物件 + 互動)* 這使得程式設計師必須建立起在 **機器模型** *(位於 "解空間" 內,這是你對問題建模的地方,例如計算機)* 和 **實際需要解決問題的模型** *(位於 "問題空間" 內,這是問題存在的地方,例如一項業務)* 之間的 **關聯**。 建立這種對映是費力的,而且這不屬於程式語言固有的功能,這使得程式難以編寫,並且維護代價高昂,同時也產生了作為副產物的整個 "程式設計方法" 行業。 ## 面向物件思想的突破 另一種對機器建模的方式就是針對待解問題建模。 早期的程式語言,例如 `LISP` 和 `APL`,都是選擇一些特定的視角來 "解釋世界" *(分別敵營 "所有問題最終都是列表" 或者 "所有問題都是演算法形式的")*。`PROLOG` 則將所有問題都轉換成決策鏈。此外還產生了基於約束條件程式設計的語言和專門通過對圖形符號操作來實現程式設計的語言 *(後來被證明限制性過強)*。 這些方式對於它們本身所要解決的 **特定型別的問題** 都是不錯的解決方案,但是一旦 **超出** 其特定領域,它們就力不從心了。 **面向物件的方式通過向程式設計師提供表示問題空間中的元素的工具而更近了一步。** 這種表示方式非常通用,使得程式設計師不會受限於任何特定型別的問題。我們把問題空間中的一些基本元素進一步抽象成解空間中的 "物件"。**這種思想的實質是:程式可以通過新增新型別的物件使其自身適用於某個特定的問題**。 因此,當你在閱讀描述解決方案的程式碼的同時,也是在閱讀問題的表述。相比之前的語言,這是一種更靈活和更強力的語言抽象。所以,OOP 允許根據問題來描述問題,而不是根據執行解決方案的計算機來描述問題。 面向物件軟體的最重要的突破之一就是允許我們按照 **自然的面向物件的大腦思維方式相匹配的方式組織軟體**。我們希望使用具有屬性並能夠與其他物件進行互動的物件,而不是直接使用更改主儲存器中的 bit 資料的機器指令。當然,在機器層面上什麼也沒有改變——bit 資料仍是由機器指令操作的,但至少我們不用再考慮機器指令了! > 對於一些規模較小的問題,將其分解為過程的開發方式比較理想。面向物件更加適合解決規模較大的問題。要想實現一個簡單的 Web 瀏覽器可能需要大約 `2000` 個過程,這些過程可能需要對一組全域性資料進行操作。 > > ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/26f7288b-2df2-41f1-a410-482a3867c548.png) > > 採用面向物件的設計風格,可能只需要大約 `100` 個類,每個類平均包含 `20` 個方法。這明顯易於程式設計師掌握,也容易找到 BUG。*(假設給定物件的資料出錯了,在訪問這個資料項的 `20` 個方法中查詢錯誤要比在 `2000` 個過程中查詢要容易多了)* ## OOP 的起源 正如我們上面描述的那樣,面向物件的程式設計是當今不可迴避的。讓我們來看看它是如何變成現實的。 時間回到上世紀 `60` 年代,那個時候計算機圖形還不存在。當時,美國電腦科學家 `Ivan Edward Sutherland` 實現了能夠繪圖的應用程式,名叫:**SketchPad**。 它是專門為設計人員開發的,它允許設計人員使用手寫筆通過計算機繪製簡單的幾何形狀,例如三角形、正方形、圓形等。該專案也是 **計算機輔助設計 CAD** 的起點。 ![SketchPad](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/c83e1ee3-dc92-4076-a30f-466d8e689fbb.png) 這成為了面向物件程式設計的 **奠基典範** 之一。 因為在 Ivan 的程式設計中,使用了我們現在稱為 "物件" 的表現形式來描述現實生活中的幾何圖形,這些圖形對於設計人員來說是完全可以理解的! 這其中沒有無窮無盡的變數和函式,而是通過具體的幾何圖形 *(物件形式)* 來描述 *(包括上下文資料,都儲存在變數中)* 和操作 *(函式實現)* 進行分組,並以一種關係進行管理這些特定的元素。 這些東西在現在都有確切的名稱。*(分別對應 "屬性" 和 "方法")* ## OOP 的規範化 Ivan 的專案和其他一些專案在 `1967` 年影響了 **Simula** 程式語言。該語言第一次直接將面向物件的思想引入到了 程式語言中 *(重大更新之後被稱為 `Simula-67`)*。 `1970` 年代,**Xerox** *(負責滑鼠和圖形介面的發明)* 在個人電腦上工作。他們希望通過操縱 GUI 和滑鼠來建立任何人都可以輕鬆使用的計算機。 ![最早的個人計算機之一](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/ebee7ece-d2db-48fc-bc92-d40ff6620a6a.png) 為了表示螢幕上的所有元素並支援其顯示和操作的邏輯,由艾倫·凱 *(Alan Kay)* 領導的團隊建立了 **SmallTalk** 語言,該語言的靈感來自 **Simula**。根據許多資料顯示,這標誌著我們今天使用的面向物件程式設計概念的正式確立! ## OOP 的普及化 上述這些方法在 `1981` 年開始流行,併成為了偉大的面嚮物件語言的起點,例如: - **Objective-C** 是 iOS 本機開發的原始語言。從那以後,Apple 對其進行了改進和增強,它仍然是 iOS 開發人員的常見選擇。 - **C ++** 是 C 程式語言的面向物件版本。C 和 C++ 仍被廣泛使用,尤其是在非常專業的行業中。 如我們所見,在程式設計方面取得了令人難以置信的進步,這是對以下問題的解決方案:**簡化軟體開發!** > **面向物件設計的特殊效率從何而來?** > > - 部分影響來自於更清晰的表達複雜系統的方式; > - 也許最重要的原因 *(也是從作業系統體系結構派生而來的)* 是,當您給某人一個結構時,您很少希望他們擁有無限的特權。僅僅進行型別匹配甚至還不能滿足需求。保護某些物件而不保護某些物件也不是非常合理有用。 > > 正確執行封裝不僅是對狀態抽象的承諾,而且是消除程式設計中面向狀態的隱喻的一種承諾。 # Part 2. 類與物件概述 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/19a4aac1-863c-4786-adf5-8350b166ed7f.png) - 圖片來源:https://javatutorial.net/java-oop **簡單的說,類是物件的藍圖或模板,而物件是類的例項。** 這個解釋雖然有點像用概念在解釋概念,但是從這句話我們至少可以看出,**類是抽象的概念,而物件是具體的東西**。 在面向物件程式設計的世界中,一切皆為物件,物件都有屬性和行為,每個物件都是獨一無二的,而且物件一定屬於某個類 *(型)*。當我們把一大堆擁有共同特徵的物件的靜態特徵 *(屬性)* 和動態特徵 *(行為)* 都抽取出來後,就可以定義出一個叫做 **“類”** 的東西。 ## 定義類 使用類幾乎可以模擬任何東西。假設我們要編寫一個表示小狗 Dog 的簡單類 —— 它表示的不是特定的小狗,而是任何小狗。 對於大多數寵物狗,我們都知道些什麼呢?—— 它們都有名字和年齡,還會叫、會吃東西。由於大多數的小狗都具備上述兩項資訊 *(名字和年齡)* 和兩種行為 *(叫和吃東西)*,所以我們的 Dog 類將包含它們,這個類看上去會是這樣: ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/a1b36ab3-0c58-451d-be4a-d280bc338f44.png) 程式碼實現起來大概會像這樣: ```java public class Dog { // 引數 private String name; private Integer age; // 構造器 public Dog(String name, Integer age) { this.name = name; this.age = age; } // 欄位訪問器 public String getName() { return name; } // 欄位訪問器 public Integer getAge() { return age; } // 方法 - 叫 void bark() { System.out.println("汪汪汪!"); } // 方法 - 吃東西 void eat() { System.out.println("一隻" + age + "歲大的名叫 " + name + " 的狗正在吃東西!"); } } ``` ### 剖析 Dog 類 下面各個部分我們將對上面描述的 Dog 類進行剖析。首先從這個類的方法開始吧,上述原始碼我們看到,這個類包含一個構造器和四個方法: ```java public Dog(String name, Integer age) public String getName() public Integer getAge() public void bark() public void eat() ``` 這個類的所有方法都被標記為 `public`。關鍵字 `public` 意味著任何類的任何方法都可以呼叫這些方法 *(共有四種訪問級別,將在之後介紹到)* 接下來,還需要注意 Dog 類例項中有 `2` 個例項欄位用來存放將要操作的資料: ```java private String name; private Integer age; ``` 關鍵字 `private` 確保只有 Dog 類自身的方法能夠訪問到這些例項欄位,而其他類的方法不能夠讀寫這些欄位。*(這也是 Private 私有本身的含義)* > **注意**:雖然可以用 `public` 標記例項欄位,但這是一種很不好的做法。`public` 修飾資料欄位後,程式中的任何方法都可以對其進行讀取和修改,這就完全破壞了 **封裝**。*(這會使程式非常不可控)* 強烈建議將實力欄位標記為 `private` 最後,請注意,**這兩個例項欄位本身也是物件**:`name` 欄位是 `String` 型別的物件,`age` 是 `Integer` 型別的物件。這種情況十分常見:類包含的例項欄位通常屬於某個類型別。 ### 從構造器開始 這個與類名相同且許可權為 `public` 的方法 `Dog()` 我們把它稱為 **構造器**,讓我們來看看它: ```java public Dog(String name, Integer age) { this.name = name; this.age = age; } ``` 在構造 Dog 類物件的時候,構造器會執行,從而將例項欄位初始化為所希望的初始狀態。 例如,當時用下面這條程式碼建立 Dog 類時: ```java new Dog("大黃", 1) ``` 將會把資料設定為: ```text name = "大黃" age = 1 ``` 構造器與其他方法有一個重要的不同。構造器總是結合 `new` 關鍵字來呼叫。不能對一個已經存在的物件呼叫構造器來達到重新設定屬性的目的。例如 *(下方程式碼將產生編譯錯誤)*: ```java dogInstance.Dog("小黃", 2); // ERROR ``` 有關構造器還有很多可以說的地方,現在只需要記住: - 構造器與類同名; - 每個類可以有一個以上的構造器; - 構造器可以有 `0` 個、`1` 個或多個引數; - 構造器沒有返回值; - 構造器總是伴隨著 `new` 操作符一起呼叫。 ### 封裝的優點 最後仔細看一下非常簡單的 `getName`/`getAge` 方法。 ```java public String getName() { return name; } public Integer getAge() { return age; } ``` 這些都是典型的訪問器方法。由於它們只返回例項欄位值,因此又稱為 **欄位訪問器**。 如果將 `name`、`age` 欄位標記為 `public`,允許任意方法訪問,而不是編寫單獨的訪問其方法,難道不是更容易一些嘛? 上面的例子似乎並不明顯 *(而且 `name` 還是一個只讀欄位)*,所以為了說明這一點,我們來舉一個更加有趣的例子。 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/c0dcc95b-b595-4c3e-803b-f4771eb1b1c0.png) 假設我們有兩個類,男人正在辛苦掙錢並時不時地檢視餘額,而此時來了一個小偷,專門偷男人的錢,**逮著一個偷一個**,而被偷了之後男人抓到了小偷,此時由於小偷的錢是私有的,**男人抓著小偷咬牙切齒卻沒有絲毫辦法可以把錢拿回來!** ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/5dcf13d6-6dff-4432-8da5-33788c151311.gif) 封裝不僅僅幫助我們提高安全性,更可以簡化操作和提高 **內聚性**。 假設你寫了一個很龐大的系統,一開始你的定義是這樣的: ```java public int age; ``` 你的程式裡大概有 `100` 條類似於這樣的語句: ```java instance.age = 10; ``` 此時突然要求你把資料型別變一下或者對這個欄位其他一些什麼統一的處理,需要修改 `100` 處的你,是不是傻了? **封裝的另一個好處是模組化**。這方便我們把散落在各處的程式碼收攏並做統一的處理。 > 設計模式器大原則之一的 **迪米特法則** 就是對於封裝的具體要求,即 A 模組使用 B 模組的某個介面行為,對 B 模組中除此行為之外的其他資訊知道得儘可能少。 > > 比如:耳塞的插孔就是提供聲音輸出的行為介面,只需要關心這個插孔是否有相應的耳塞標記,是否是圓形,有沒有聲音即可,至於內部 CPU 如何運算音訊資訊,以及各個電容如何協同工作,根本不需要關注,這使得模組之間的協作只需忠於介面、忠於功能實現即可。 ## 建立和使用類 定義了 `class` 只是定義了物件模板,而要根據模板創建出真正的物件例項,必須使用 `new` 關鍵字,並呼叫物件的建構函式才行: ```java Dog dog = new Dog("大黃", 1); ``` 上述程式碼建立了一個 `Dog` 型別的例項,並通過變數 `dog` 指向它。*(下面我們將詳細說明是怎麼 "指向" 它的...)* 第一個 `Dog` 表明了 `dog` 變數的型別,第二個 `Dog` 則是呼叫了 `Dog` 類的建構函式。在 **Java 10** 中,如果可以從變數的初始值推匯出它們的型別,那麼可以用 `var` 關鍵字來宣告區域性變數,而無須指定型別。例如: ```java var dog = new Dog("大黃", 1); ``` 這一點很好,因為可以避免重複寫型別名 `Dog`。但是引數和欄位的型別還是必須顯式地宣告,該用法僅能用於方法中的區域性變數。 要想使用類中公用方法,我們可以直接使用 `.` *(英文句號)* 來連線類中的方法並呼叫: ```java dog.eat(); // 呼叫該例項的 eat() 方法 ``` ## Java 使用引用來操縱物件 每種程式語言都有自己的操縱記憶體中元素的方式。有時候,程式設計師必須注意將要處理的資料是什麼型別。你是直接操縱元素,還是用某種基於特殊語法的間接表示 *(例如 C 和 C++ 裡的指標)* 來操縱物件? **在 Java 中一切都被視為物件**,這使得我們可以使用固定的語法。儘管這一切都看作物件,但操縱的識別符號 *(例如上面的 `dog` 變數)* 實際上是 **物件的一個 "引用"** *(reference)*。 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/d319dcdb-166a-4804-b16c-785721d07b06.png) 只要握住這個遙控器,就能保持與電視機的連線。當有人想改變頻道或減小音量時,實際操縱的是遙控器 *(引用)*,再由控制器來調控電視機 *(物件)*。 如果想在房間裡四處走走,同時仍能調控電視機,那麼只需要攜帶遙控器就可以了,而不是揹著電視機... **此外,即使沒有電視機,遙控器也可以獨立存在**。 也就是說,你擁有一個引用,並不一定需要有一個物件與它關聯。因此,如果你想操縱一個詞或者一個句子,則可以建立一個 **String** 物件: ```java String s; ``` 但是這裡建立的只是引用,並不是物件。如果此時向 `s` 傳送一個訊息,就會返回一個執行時錯誤。這是因為此時 `s` 實際上沒有與任何事物相關聯 *(即沒有電視機)*。 **因此,一種安全的做法是:建立一個引用的同時便進行初始化。** ```java String s = "abcd"; ``` 這裡運用到了 Java 語言的一個特性:字串可以直接使用帶引號的文字進行初始化 *(其他物件需要使用 `new`)*。 ### null 引用 上面我們已經瞭解到,一個物件變數包含一個物件的引用。當引用沒有關聯物件時,實際上指向了一個特殊的值 `null`,這表示它沒有引用任何物件。*(可以理解為 `String s;` 等同於 `String s = null;`)* 聽上去這是一種處理特殊情況的便捷機制,如未知的名字。但使用 `null` 值需要非常小心!如果對 `null` 值應用一個方法,那麼就會產生一個 `NullPointException` 異常。 ```java String s = null; System.out.println(s.length()); // NullPointException ``` 這是一個很嚴重的錯誤!如果你的程式沒有 "捕獲" *(理解為手動檢測和處理)* 異常,程式就會終止!正常情況下,程式並不會捕獲這些異常,而是依賴於程式設計師從一開始就不要帶來異常。*(這顯然很難..)* 定義一個類時,最好清楚的知道哪些欄位可能為 `null`。在我們的例子中 *(Dog 類)*,我們不希望 `name` 和 `age` 欄位為 `null`。 對此我們有兩種解決方法。 **"寬容型" 方法** 是把 `null` 引數轉換為一個適當的非 `null` 值: ```java if (n == null) { name = "unknow"; } else { name = n; } ``` 在 **Java 9** 中,`Objects` 類 *(JDK 自帶的工具類)* 對此提供了一個便利方法: ```java name = Objects.requireNonNullElse(n, "unknow"); // 效果與上面程式碼等同 ``` **"嚴格型" 方法** 則是乾脆拒絕 `null` 引數: ```java name = Objects.requireNonNull(n, "The name cannot be null!"); ``` 如果把上述程式碼新增進 `Dog` 類的建構函式,並且有人用 `null` 名字構造了一個 `Dog` 類,就會產生一個 `NullPointerException` 異常。乍看上去,這種做法似乎不太好,但有以下幾個好處: 1. 異常報告會提供這個問題的描述;*(也就是 `The name cannot be null!`)* 2. 異常報告會準確地支出問題所在的位置,否則異常可能在其他地方出現,而很難追蹤到真正導致問題的這個構造器引數; # Part 3. 面向物件的四大特性 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/cfeaa75b-ab0f-493d-8173-20e49cbcac1b.png) - 圖片來源:https://flashgene.com/archives/51547.html 面向物件有三大特性:**封裝**、**繼承**、**多型**。有的地方支援把 **"抽象"** 也歸納進來,合併稱為面向物件的四大特性。我覺得也無可厚非。 *(關於繼承和多型會在後續章節裡面詳細說明, 這裡只作簡單描述用於簡單理解..)* ## 抽象 抽象是面相物件思想最基礎的能力之一,正確而嚴謹的業務抽象和建模分析能力是後續的封裝、繼承、多型的基礎,是軟體大廈的基石。*(上面有專門的一節描述,這裡不再展開)* ## 封裝 正如我們上面 **男人與小偷** 的例子,封裝不僅能提高我們的安全性、幫助我們把實現細節隱藏起來,還是一種物件功能內聚的表現形式,這有助於讓模組之間的耦合度變低,也更具有維護性。*(封裝的優點上方有介紹,這裡也不再展開)* 封裝使面向物件的世界變得單純,物件之間的關係變得簡單,"自掃門前雪" 就行了。特別是當今智慧化的時代,對封裝的要求越來越高了,例如 **小愛同學** 好了,對外的唯一介面就是語音輸入,隱藏了指令內部的細節實現和相關資料,這大大降低了使用成本,也有效地保護了內部資料安全。 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/5edae3e9-dbd8-4562-8fab-945b36eb68d4.png) ## 繼承 繼承允許建立 **具有邏輯等級結構的類體系**,形成一個繼承樹。就拿我們上面建立的 **Dog** 類來說明吧,不是隻有狗擁有那些屬性和方法,貓也有!*(可能貓叫不能用 `bark` 表示,但本質都是叫)* 自然界中,有許多動物 *(動物是對這些生物的自然抽象)* 都有這樣的行為,那麼好了,我們往上再抽象一個 `Animal` 物件: ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/f4970969-e654-4bed-957b-fe5a3881cbc4.png) 只要繼承自 `Animal` 類,那麼就會擁有 `Animal` 這個父類所描述的屬性和方法 *(子類當然可以有自己的實現,這一點我們在後續章節中詳細描述)*。這讓軟體在業務多變的客觀條件下,某些基礎模組可以被直接複用、間接複用或增強複用。 **繼承把枯燥的程式碼世界變得更有層次感,更具有擴充套件性,為多型打下了語法基礎。** 不過繼承也有幾個 **缺點**: 1. 繼承是一種 **強耦合** 的關係,父類如果做出一定改變,那麼子類也必然會改變; 2. 繼承 **破壞了封裝**,對於子類而言,它的實現對子類來說都是透明的; ## 多型 **多型是以上述的三個面向物件特徵為基礎**,根據執行時的實際物件型別,同一個方法產生不同的執行結果,使同一個行為具有不同的表現形式。 太學術化了一點,舉個例子可能明白點。比如,有一杯水,我不知道它是溫的、冰的還是燙的,但是我一摸我就知道了,我摸水杯的這個動作 *(方法)*,對於不同溫度的水 *(執行時不同的物件型別)*,就會得到不同的結果,這就是多型。 ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/「MoreThanJava」Day4:面向物件基礎/ae55c7cc-ea6e-45f6-8014-307eb774bb38.png) 自然界中最典型的例子就是碳家族。如果你告訴你的女朋友將在她的生日晚會上送她一塊碳,女朋友當然不高興了,可事實上卻是 5 克拉的鑽石。鑽石就是碳元素在不斷進化過程中的一種多型表現。 嚴格意義來說,多型並不是面向物件的一種特質,而是一種由繼承行為衍生而來的進化能力而已。 (完) # 要點回顧 1. 類和物件 - 什麼是類 / 什麼是物件 / OOP 起源和發展 / 面向物件其他相關概念 2. 定義類 - 基本結構 / 屬性和方法 / 構造器 3. 使用物件 - 建立物件 / 給物件發訊息 4. 面向物件的四大支柱 - 抽象 / 封裝 / 繼承 / 多型的簡單介紹 5. 基礎練習 - 定義 Dog 類 / 定義時鐘類 / 定義圖形類 *(下方)* # 練習 ## 練習 1:定義一個類描述數字時鐘 參考答案: ```java public class Clock { private Integer hour; private Integer minute; private Integer second; public Clock(Integer hour, Integer minute, Integer second) { this.hour = hour; this.minute = minute; this.second = second; } /** * 時鐘走字(走1s) */ public void run() { second += 1; if (second.equals(60)) { second = 0; minute += 1; if (minute.equals(60)) { minute = 0; hour += 1; if (hour.equals(24)) { hour = 0; } } } } /** * 顯示當前時間 * @return */ public String showCurrentTime() { return String.format("當前時間是:%d時:%d分:%d秒", hour, minute, second); } /** * 內部測試 * @throws InterruptedException - 使用 Thread.sleep() 需要手動檢測該異常, 這裡節約篇幅直接丟擲 */ public static void main(String[] args) throws InterruptedException { Clock clock = new Clock(23, 59, 58); while (true) { clock.run(); System.out.println(clock.showCurrentTime()); // 讓當前執行緒睡 1s Thread.sleep(1000); } } } ``` ## 練習 2:定義一個類描述平面上的點並提供移動點和計算到另一個點距離的方法 參考答案: ```java public class Point { private Integer x; private Integer y; public Point() { this.x = 0; this.y = 0; } public Point(Integer x, Integer y) { this.x = x; this.y = y; } /** * 移動到指定位置 */ public void moveTo(Integer x, Integer y) { this.x = x; this.y = y; } /** * 移動指定的距離 */ public void moveBy(Integer dx, Integer dy) { this.x += dx; this.y += dy; } /** * 計算並返回與另一個點的距離 */ public Double distanceTo(Point other) { int dx = this.x - other.x; int dy = this.y - other.y; return Math.sqrt(dx ^ 2 + dy ^ 2); } /** * 當前的座標資訊 */ public String currentLocation() { return String.format("當前點橫座標:%d,縱座標:%d", x, y); } /** * 內部測試 */ public static void main(String[] args) { Point point1 = new Point(3, 5); Point point2 = new Point(); System.out.println(point1.currentLocation()); System.out.println(point2.currentLocation()); point2.moveTo(-1, 2); System.out.println(point2.currentLocation()); System.out.println(point1.distanceTo(point2)); } } ``` # 參考資料 1. 《Java 核心技術 卷 I》 1. 《Java 程式設計思想》 1. 《碼出高效 Java 開發手冊》 1. Deepen your knowledge by learning Object Oriented Programming (OOP) with Swift - https://openclassrooms.com/en/courses/4542221-deepen-your-knowledge-by-learning-object-oriented-programming-oop-with-swift 1. Think like a computer: the logic of programming - https://openclassrooms.com/en/courses/5261196-think-like-a-computer-the-logic-of-programming 1. Introduction to Computer Science using Java - http://programmedlessons.org/Java9/index.html#part02 1. Python 100 天從新手到大師 - https://github.com/jackfrued/Python-100-Days > - 本文已收錄至我的 Github 程式設計師成長系列 **【More Than Java】,學習,不止 Code,歡迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** > - **個人公眾號** :wmyskxz,**個人獨立域名部落格**:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長! ![](https://cdn.jsdelivr.net/gh/wmyskxz/img/img/common/qrcode.png) 非常感謝各位人才能 **看到這裡**,如果覺得本篇文章寫得不錯,覺得 **「我沒有三顆心臟」有點東西** 的話,**求點贊,求關注,求分享,求留言!** 創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見!