「MoreThanJava」Day 4:面向對象基礎

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高品質的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支援是我前進的最大的動力!

Part 1. 面向對象設計概述

面向對象程式設計 (Object-Oriented Programming, OOP) 是當今主流的程式設計范型,它取代了 20 世紀 70 年代的 “結構化” 或過程式編程技術。由於 Java 是面向對象的,所以必須熟悉 OOP 才能夠很好地使用 Java。

了解抽象

抽象的作用是將複雜的機制隱藏在一個對象中,僅保留我們與之交互所必須的資訊

為了說明這一點,我們可以想像平時使用 「電梯」 的場景。

熟悉的早晨等電梯!

如果您在辦公樓工作,這可能是您日常工作的一部分。你按下向上或向下按鈕,然後等待門滑開。完成操作後,您進入一個 “盒子”,該 “盒子” 的一面牆上有一個按鈕面板,然後按下所需的按鈕。當電梯到達您要去到的樓層後,您會擠過其他人然後走出去。

要使用電梯,您只需要了解如何按下正確的按鈕就可以達到目的。

而隱藏在電梯背後的支援它工作的一系列東西 —— 滑輪系統、機械、電線、減震器、安全系統等等… 您可以完全不知道也完全不必操心…

我完全不知道他們在做什麼...

電梯這個 “鐵盒子” 以及相應的按鈕面板,就是對整個「運輸系統」成功的抽象 (事實上電梯背後還包含檢修、維護等一系列事情…),它隱藏了足夠多的細節,也極大地方便了我們的生活。

什麼是對象

簡單來說,對象是對現實世界的抽象。 (例如上方對整個運輸系統抽象之後,就得到了「電梯」這個對象…)

什麼東西是對象?什麼東西不是對象?這是一個困擾哲學家數千年的問題。勒內·笛卡爾 (17世紀的哲學家) 觀察到,人類是用面向對象的方式看待世界的 (例如與電梯的交互)。人類的大腦會從對象的角度認識世界 (例如鳥類、魚類),我們的思想和記憶也被組織成物體和它們之間的關係 (例如,鳥吃蟲)

對象像是一種模板

亞里士多德大概是第一個深入研究 類型 (type) 的哲學家,它曾經提出過 魚類鳥類 這樣的概念。所有的對象都是唯一的,但同時也是具有相同的特性和行為的對象所歸屬的類的一部分。

這就好像我們拿著一個模具,我們可以使用該模具製作出各種各樣東西,每個東西都有自己的 “個性”,但它們又都遵循一些相同的基本模式:

對象的特徵

我們可以把你的「銀行賬戶」抽象成一個對象,但它不是由物質構成的。(雖然您和銀行可以使用紙張和其他材料來記錄您的賬戶,但您的賬戶獨立於這些材料而存在。)

雖然它不是物質的,但你的賬戶是有 屬性(餘額、利率、持有者等..)你可以對它做一些事情 (存款、取款、查看餘額等..,)它自己也可以做一些事情 (交易收費、積累利息等…)

這足夠清楚吧。事實上,這些特徵它們都有名字:

  • 對象具有 標識 identity(每個對象都是獨立的個體)
  • 對象具有 狀態 state(它具有各種可能會改變的屬性)
  • 對象具有 行為 behavior(它可以做事情,也可以讓別人對它做事情)

這就是對一個物體的一般描述。(上面的列表來自於 1994Grady Booch/Addison-Wesley 出版的《面向對象分析與設計》一書。) 當你開始編寫面向對象的軟體時,你會發現這個列表將幫助你決定你的對象應該是什麼樣。

程式語言中的抽象過程

所有程式語言都提供抽象機制。可以認為,人們所能夠解決的問題的複雜性直接取決於抽象的類型和品質

所謂的 “類型” 是指 “所抽象的是什麼?”。

彙編語言是對底層機器語言的輕微抽象,接著出現的許多 “命令式” 語言 (如 FORTRAN、BASIC、C 等..) 都是對彙編語言的進一步抽象。

這些語言在彙編語言基礎上有了很大幅度的改進,但是它們所作的主要抽象仍要求在解決問題時要基於電腦的結構,而不是基於所要解決的問題的結構來考慮。

傳統的結構化程式設計通過設計一系列的過程 (即演算法) 來求解問題。一旦確定了這些過程,就要開始考慮存儲數據的適當方式。

這就是 Pascal 語言的設計者 Niklaus Wirth 將其著作命名為《演算法 + 數據結構 = 程式》(Algorithms + Data Structures = Programs, Prentice Hall, 1975) 的原因。

需要注意的是,在 Wirth 的這個書名中,演算法是第一位的,數據結構是第二位的,這就明確的表述了程式設計師的工作方式。首先要確定如何操作數據,然後再決定如何組織數據的結構,以便於操作數據。

而 OOP 卻調換了這個次序,將數據放在第一位,然後再考慮操作數據的演算法。(在 OOP 中,也有說法是:程式 = 對象 + 交互)

這使得程式設計師必須建立起在 機器模型 (位於 “解空間” 內,這是你對問題建模的地方,例如電腦)實際需要解決問題的模型 (位於 “問題空間” 內,這是問題存在的地方,例如一項業務) 之間的 關聯

建立這種映射是費力的,而且這不屬於程式語言固有的功能,這使得程式難以編寫,並且維護代價高昂,同時也產生了作為副產物的整個 “編程方法” 行業。

面向對象思想的突破

另一種對機器建模的方式就是針對待解問題建模。

早期的程式語言,例如 LISPAPL,都是選擇一些特定的視角來 “解釋世界” (分別敵營 “所有問題最終都是列表” 或者 “所有問題都是演算法形式的”)PROLOG 則將所有問題都轉換成決策鏈。此外還產生了基於約束條件編程的語言和專門通過對圖形符號操作來實現編程的語言 (後來被證明限制性過強)

這些方式對於它們本身所要解決的 特定類型的問題 都是不錯的解決方案,但是一旦 超出 其特定領域,它們就力不從心了。

面向對象的方式通過向程式設計師提供表示問題空間中的元素的工具而更近了一步。

這種表示方式非常通用,使得程式設計師不會受限於任何特定類型的問題。我們把問題空間中的一些基本元素進一步抽象成解空間中的 “對象”。這種思想的實質是:程式可以通過添加新類型的對象使其自身適用於某個特定的問題

因此,當你在閱讀描述解決方案的程式碼的同時,也是在閱讀問題的表述。相比之前的語言,這是一種更靈活和更強力的語言抽象。所以,OOP 允許根據問題來描述問題,而不是根據運行解決方案的電腦來描述問題。

面向對象軟體的最重要的突破之一就是允許我們按照 自然的面向對象的大腦思維方式相匹配的方式組織軟體。我們希望使用具有屬性並能夠與其他對象進行交互的對象,而不是直接使用更改主存儲器中的 bit 數據的機器指令。當然,在機器層面上什麼也沒有改變——bit 數據仍是由機器指令操作的,但至少我們不用再考慮機器指令了!

對於一些規模較小的問題,將其分解為過程的開發方式比較理想。面向對象更加適合解決規模較大的問題。要想實現一個簡單的 Web 瀏覽器可能需要大約 2000 個過程,這些過程可能需要對一組全局數據進行操作。

採用面向對象的設計風格,可能只需要大約 100 個類,每個類平均包含 20 個方法。這明顯易於程式設計師掌握,也容易找到 BUG。(假設給定對象的數據出錯了,在訪問這個數據項的 20 個方法中查找錯誤要比在 2000 個過程中查找要容易多了)

OOP 的起源

正如我們上面描述的那樣,面向對象的編程是當今不可迴避的。讓我們來看看它是如何變成現實的。

時間回到上世紀 60 年代,那個時候電腦圖形還不存在。當時,美國電腦科學家 Ivan Edward Sutherland 實現了能夠繪圖的應用程式,名叫:SketchPad

它是專門為設計人員開發的,它允許設計人員使用手寫筆通過電腦繪製簡單的幾何形狀,例如三角形、正方形、圓形等。該項目也是 電腦輔助設計 CAD 的起點。

SketchPad

這成為了面向對象編程的 奠基典範 之一。

因為在 Ivan 的程式設計中,使用了我們現在稱為 “對象” 的表現形式來描述現實生活中的幾何圖形,這些圖形對於設計人員來說是完全可以理解的!

這其中沒有無窮無盡的變數和函數,而是通過具體的幾何圖形 (對象形式) 來描述 (包括上下文數據,都存儲在變數中) 和操作 (函數實現) 進行分組,並以一種關係進行管理這些特定的元素。

這些東西在現在都有確切的名稱。(分別對應 “屬性” 和 “方法”)

OOP 的規範化

Ivan 的項目和其他一些項目在 1967 年影響了 Simula 程式語言。該語言第一次直接將面向對象的思想引入到了 程式語言中 (重大更新之後被稱為 Simula-67)

1970 年代,Xerox (負責滑鼠和圖形介面的發明) 在個人電腦上工作。他們希望通過操縱 GUI 和滑鼠來創建任何人都可以輕鬆使用的電腦。

最早的個人電腦之一

為了表示螢幕上的所有元素並支援其顯示和操作的邏輯,由艾倫·凱 (Alan Kay) 領導的團隊創建了 SmallTalk 語言,該語言的靈感來自 Simula。根據許多資料顯示,這標誌著我們今天使用的面向對象編程概念的正式確立!

OOP 的普及化

上述這些方法在 1981 年開始流行,並成為了偉大的面向對象語言的起點,例如:

  • Objective-C 是 iOS 本機開發的原始語言。從那以後,Apple 對其進行了改進和增強,它仍然是 iOS 開發人員的常見選擇。
  • C ++ 是 C 程式語言的面向對象版本。C 和 C++ 仍被廣泛使用,尤其是在非常專業的行業中。

如我們所見,在編程方面取得了令人難以置信的進步,這是對以下問題的解決方案:簡化軟體開發!

面向對象設計的特殊效率從何而來?

  • 部分影響來自於更清晰的表達複雜系統的方式;
  • 也許最重要的原因 (也是從作業系統體系結構派生而來的) 是,當您給某人一個結構時,您很少希望他們擁有無限的特權。僅僅進行類型匹配甚至還不能滿足需求。保護某些對象而不保護某些對象也不是非常合理有用。

正確執行封裝不僅是對狀態抽象的承諾,而且是消除編程中面向狀態的隱喻的一種承諾。

Part 2. 類與對象概述

簡單的說,類是對象的藍圖或模板,而對象是類的實例。

這個解釋雖然有點像用概念在解釋概念,但是從這句話我們至少可以看出,類是抽象的概念,而對象是具體的東西

在面向對象編程的世界中,一切皆為對象,對象都有屬性和行為,每個對象都是獨一無二的,而且對象一定屬於某個類 (型)。當我們把一大堆擁有共同特徵的對象的靜態特徵 (屬性) 和動態特徵 (行為) 都抽取出來後,就可以定義出一個叫做 「類」 的東西。

定義類

使用類幾乎可以模擬任何東西。假設我們要編寫一個表示小狗 Dog 的簡單類 —— 它表示的不是特定的小狗,而是任何小狗。

對於大多數寵物狗,我們都知道些什麼呢?—— 它們都有名字和年齡,還會叫、會吃東西。由於大多數的小狗都具備上述兩項資訊 (名字和年齡) 和兩種行為 (叫和吃東西),所以我們的 Dog 類將包含它們,這個類看上去會是這樣:

程式碼實現起來大概會像這樣:

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 類進行剖析。首先從這個類的方法開始吧,上述源碼我們看到,這個類包含一個構造器和四個方法:

public Dog(String name, Integer age)
public String getName()
public Integer getAge()
public void bark()
public void eat()

這個類的所有方法都被標記為 public。關鍵字 public 意味著任何類的任何方法都可以調用這些方法 (共有四種訪問級別,將在之後介紹到)

接下來,還需要注意 Dog 類實例中有 2 個實例欄位用來存放將要操作的數據:

private String name;
private Integer age;

關鍵字 private 確保只有 Dog 類自身的方法能夠訪問到這些實例欄位,而其他類的方法不能夠讀寫這些欄位。(這也是 Private 私有本身的含義)

注意:雖然可以用 public 標記實例欄位,但這是一種很不好的做法。public 修飾數據欄位後,程式中的任何方法都可以對其進行讀取和修改,這就完全破壞了 封裝(這會使程式非常不可控) 強烈建議將實力欄位標記為 private

最後,請注意,這兩個實例欄位本身也是對象name 欄位是 String 類型的對象,ageInteger 類型的對象。這種情況十分常見:類包含的實例欄位通常屬於某個類類型。

從構造器開始

這個與類名相同且許可權為 public 的方法 Dog() 我們把它稱為 構造器,讓我們來看看它:

public Dog(String name, Integer age) {
    this.name = name;
    this.age = age;
}

在構造 Dog 類對象的時候,構造器會運行,從而將實例欄位初始化為所希望的初始狀態。

例如,當時用下面這條程式碼創建 Dog 類時:

new Dog("大黃", 1)

將會把數據設置為:

name = "大黃"
age = 1

構造器與其他方法有一個重要的不同。構造器總是結合 new 關鍵字來調用。不能對一個已經存在的對象調用構造器來達到重新設置屬性的目的。例如 (下方程式碼將產生編譯錯誤)

dogInstance.Dog("小黃", 2);  // ERROR

有關構造器還有很多可以說的地方,現在只需要記住:

  • 構造器與類同名;
  • 每個類可以有一個以上的構造器;
  • 構造器可以有 0 個、1 個或多個參數;
  • 構造器沒有返回值;
  • 構造器總是伴隨著 new 操作符一起調用。

封裝的優點

最後仔細看一下非常簡單的 getName/getAge 方法。

public String getName() {
    return name;
}
public Integer getAge() {
    return age;
}

這些都是典型的訪問器方法。由於它們只返回實例欄位值,因此又稱為 欄位訪問器

如果將 nameage 欄位標記為 public,允許任意方法訪問,而不是編寫單獨的訪問其方法,難道不是更容易一些嘛?

上面的例子似乎並不明顯 (而且 name 還是一個只讀欄位),所以為了說明這一點,我們來舉一個更加有趣的例子。

假設我們有兩個類,男人正在辛苦掙錢並時不時地查看餘額,而此時來了一個小偷,專門偷男人的錢,逮著一個偷一個,而被偷了之後男人抓到了小偷,此時由於小偷的錢是私有的,男人抓著小偷咬牙切齒卻沒有絲毫辦法可以把錢拿回來!

封裝不僅僅幫助我們提高安全性,更可以簡化操作和提高 內聚性

假設你寫了一個很龐大的系統,一開始你的定義是這樣的:

public int age;

你的程式里大概有 100 條類似於這樣的語句:

instance.age = 10;

此時突然要求你把數據類型變一下或者對這個欄位其他一些什麼統一的處理,需要修改 100 處的你,是不是傻了?

封裝的另一個好處是模組化。這方便我們把散落在各處的程式碼收攏並做統一的處理。

設計模式器大原則之一的 迪米特法則 就是對於封裝的具體要求,即 A 模組使用 B 模組的某個介面行為,對 B 模組中除此行為之外的其他資訊知道得儘可能少。

比如:耳塞的插孔就是提供聲音輸出的行為介面,只需要關心這個插孔是否有相應的耳塞標記,是否是圓形,有沒有聲音即可,至於內部 CPU 如何運算音頻資訊,以及各個電容如何協同工作,根本不需要關注,這使得模組之間的協作只需忠於介面、忠於功能實現即可。

創建和使用類

定義了 class 只是定義了對象模板,而要根據模板創建出真正的對象實例,必須使用 new 關鍵字,並調用對象的構造函數才行:

Dog dog = new Dog("大黃", 1);

上述程式碼創建了一個 Dog 類型的實例,並通過變數 dog 指向它。(下面我們將詳細說明是怎麼 “指向” 它的…)

第一個 Dog 表明了 dog 變數的類型,第二個 Dog 則是調用了 Dog 類的構造函數。在 Java 10 中,如果可以從變數的初始值推導出它們的類型,那麼可以用 var 關鍵字來聲明局部變數,而無須指定類型。例如:

var dog = new Dog("大黃", 1);

這一點很好,因為可以避免重複寫類型名 Dog。但是參數和欄位的類型還是必須顯式地聲明,該用法僅能用於方法中的局部變數。

要想使用類中公用方法,我們可以直接使用 . (英文句號) 來連接類中的方法並調用:

dog.eat();  // 調用該實例的 eat() 方法

Java 使用引用來操縱對象

每種程式語言都有自己的操縱記憶體中元素的方式。有時候,程式設計師必須注意將要處理的數據是什麼類型。你是直接操縱元素,還是用某種基於特殊語法的間接表示 (例如 C 和 C++ 里的指針) 來操縱對象?

在 Java 中一切都被視為對象,這使得我們可以使用固定的語法。儘管這一切都看作對象,但操縱的標識符 (例如上面的 dog 變數) 實際上是 對象的一個 “引用” (reference)

只要握住這個遙控器,就能保持與電視機的連接。當有人想改變頻道或減小音量時,實際操縱的是遙控器 (引用),再由控制器來調控電視機 (對象)

如果想在房間里四處走走,同時仍能調控電視機,那麼只需要攜帶遙控器就可以了,而不是背著電視機…

此外,即使沒有電視機,遙控器也可以獨立存在

也就是說,你擁有一個引用,並不一定需要有一個對象與它關聯。因此,如果你想操縱一個詞或者一個句子,則可以創建一個 String 對象:

String s;

但是這裡創建的只是引用,並不是對象。如果此時向 s 發送一個消息,就會返回一個運行時錯誤。這是因為此時 s 實際上沒有與任何事物相關聯 (即沒有電視機)

因此,一種安全的做法是:創建一個引用的同時便進行初始化。

String s = "abcd";

這裡運用到了 Java 語言的一個特性:字元串可以直接使用帶引號的文本進行初始化 (其他對象需要使用 new)

null 引用

上面我們已經了解到,一個對象變數包含一個對象的引用。當引用沒有關聯對象時,實際上指向了一個特殊的值 null,這表示它沒有引用任何對象。(可以理解為 String s; 等同於 String s = null;)

聽上去這是一種處理特殊情況的便捷機制,如未知的名字。但使用 null 值需要非常小心!如果對 null 值應用一個方法,那麼就會產生一個 NullPointException 異常。

String s = null;
System.out.println(s.length());  // NullPointException

這是一個很嚴重的錯誤!如果你的程式沒有 “捕獲” (理解為手動檢測和處理) 異常,程式就會終止!正常情況下,程式並不會捕獲這些異常,而是依賴於程式設計師從一開始就不要帶來異常。(這顯然很難..)

定義一個類時,最好清楚的知道哪些欄位可能為 null。在我們的例子中 (Dog 類),我們不希望 nameage 欄位為 null

對此我們有兩種解決方法。

“寬容型” 方法 是把 null 參數轉換為一個適當的非 null 值:

if (n == null) {
    name = "unknow";
} else {
    name = n; 
}

Java 9 中,Objects(JDK 自帶的工具類) 對此提供了一個便利方法:

name = Objects.requireNonNullElse(n, "unknow");  // 效果與上面程式碼等同

“嚴格型” 方法 則是乾脆拒絕 null 參數:

name = Objects.requireNonNull(n, "The name cannot be null!");

如果把上述程式碼添加進 Dog 類的構造函數,並且有人用 null 名字構造了一個 Dog 類,就會產生一個 NullPointerException 異常。乍看上去,這種做法似乎不太好,但有以下幾個好處:

  1. 異常報告會提供這個問題的描述;(也就是 The name cannot be null!)
  2. 異常報告會準確地支出問題所在的位置,否則異常可能在其他地方出現,而很難追蹤到真正導致問題的這個構造器參數;

Part 3. 面向對象的四大特性

面向對象有三大特性:封裝繼承多態。有的地方支援把 “抽象” 也歸納進來,合併稱為面向對象的四大特性。我覺得也無可厚非。

(關於繼承和多態會在後續章節裡面詳細說明, 這裡只作簡單描述用於簡單理解..)

抽象

抽象是面相對象思想最基礎的能力之一,正確而嚴謹的業務抽象和建模分析能力是後續的封裝、繼承、多態的基礎,是軟體大廈的基石。(上面有專門的一節描述,這裡不再展開)

封裝

正如我們上面 男人與小偷 的例子,封裝不僅能提高我們的安全性、幫助我們把實現細節隱藏起來,還是一種對象功能內聚的表現形式,這有助於讓模組之間的耦合度變低,也更具有維護性。(封裝的優點上方有介紹,這裡也不再展開)

封裝使面向對象的世界變得單純,對象之間的關係變得簡單,”自掃門前雪” 就行了。特別是當今智慧化的時代,對封裝的要求越來越高了,例如 小愛同學 好了,對外的唯一介面就是語音輸入,隱藏了指令內部的細節實現和相關數據,這大大降低了使用成本,也有效地保護了內部數據安全。

繼承

繼承允許創建 具有邏輯等級結構的類體系,形成一個繼承樹。就拿我們上面創建的 Dog 類來說明吧,不是只有狗擁有那些屬性和方法,貓也有!(可能貓叫不能用 bark 表示,但本質都是叫) 自然界中,有許多動物 (動物是對這些生物的自然抽象) 都有這樣的行為,那麼好了,我們往上再抽象一個 Animal 對象:

只要繼承自 Animal 類,那麼就會擁有 Animal 這個父類所描述的屬性和方法 (子類當然可以有自己的實現,這一點我們在後續章節中詳細描述)。這讓軟體在業務多變的客觀條件下,某些基礎模組可以被直接復用、間接復用或增強復用。

繼承把枯燥的程式碼世界變得更有層次感,更具有擴展性,為多態打下了語法基礎。

不過繼承也有幾個 缺點

  1. 繼承是一種 強耦合 的關係,父類如果做出一定改變,那麼子類也必然會改變;
  2. 繼承 破壞了封裝,對於子類而言,它的實現對子類來說都是透明的;

多態

多態是以上述的三個面向對象特徵為基礎,根據運行時的實際對象類型,同一個方法產生不同的運行結果,使同一個行為具有不同的表現形式。

太學術化了一點,舉個例子可能明白點。比如,有一杯水,我不知道它是溫的、冰的還是燙的,但是我一摸我就知道了,我摸水杯的這個動作 (方法),對於不同溫度的水 (運行時不同的對象類型),就會得到不同的結果,這就是多態。

自然界中最典型的例子就是碳家族。如果你告訴你的女朋友將在她的生日晚會上送她一塊碳,女朋友當然不高興了,可事實上卻是 5 克拉的鑽石。鑽石就是碳元素在不斷進化過程中的一種多態表現。

嚴格意義來說,多態並不是面向對象的一種特質,而是一種由繼承行為衍生而來的進化能力而已。

(完)

要點回顧

  1. 類和對象 – 什麼是類 / 什麼是對象 / OOP 起源和發展 / 面向對象其他相關概念
  2. 定義類 – 基本結構 / 屬性和方法 / 構造器
  3. 使用對象 – 創建對象 / 給對象發消息
  4. 面向對象的四大支柱 – 抽象 / 封裝 / 繼承 / 多態的簡單介紹
  5. 基礎練習 – 定義 Dog 類 / 定義時鐘類 / 定義圖形類 (下方)

練習

練習 1:定義一個類描述數字時鐘

參考答案:

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:定義一個類描述平面上的點並提供移動點和計算到另一個點距離的方法

參考答案:

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》
  2. 《Java 編程思想》
  3. 《碼出高效 Java 開發手冊》
  4. Deepen your knowledge by learning Object Oriented Programming (OOP) with Swift – //openclassrooms.com/en/courses/4542221-deepen-your-knowledge-by-learning-object-oriented-programming-oop-with-swift
  5. Think like a computer: the logic of programming – //openclassrooms.com/en/courses/5261196-think-like-a-computer-the-logic-of-programming
  6. Introduction to Computer Science using Java – //programmedlessons.org/Java9/index.html#part02
  7. Python 100 天從新手到大師 – //github.com/jackfrued/Python-100-Days
  • 本文已收錄至我的 Github 程式設計師成長系列 【More Than Java】,學習,不止 Code,歡迎 star://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名部落格:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裡,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見!