「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 - https://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 - https://openclassrooms.com/en/courses/5261196-think-like-a-computer-the-logic-of-programming
  6. Introduction to Computer Science using Java - http://programmedlessons.org/Java9/index.html#part02
  7. Python 100 天從新手到大師 - https://github.com/jackfrued/Python-100-Days
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM