跳轉至內容

Java之道/建立你自己的物件

來自Wikibooks,開放世界中的開放書籍

建立你自己的物件

[編輯 | 編輯原始碼]

類定義和物件型別

[編輯 | 編輯原始碼]

每次編寫類定義時,都會建立一個新的物件型別,其名稱與類相同。早在“Hello”部分,當我們定義名為Hello的類時,我們也建立了一個名為Hello的物件型別。我們沒有建立任何型別為Hello的變數,也沒有使用new命令建立任何Hello物件,但我們可以這樣做!

這個例子可能沒有任何意義,因為沒有理由建立Hello物件,而且如果我們確實建立了它,它有什麼用也不清楚。在本章中,我們將研究一些建立有用新物件型別的類定義示例。

以下是本章中最重要的思想

  • 定義一個新類也會建立一個新的物件型別

名稱相同。

  • 類定義就像物件的模板

它確定物件具有哪些例項變數以及哪些方法可以對其進行操作。

  • 每個物件都屬於某種物件型別;因此,它

是某個類的例項。

  • 當你呼叫new命令建立物件時,Java

呼叫一個名為建構函式的特殊方法來初始化例項變數。你可以在類定義中提供一個或多個建構函式。

  • 通常,所有操作型別的方法都在

該型別的類定義中。

以下是關於類定義的一些語法問題

  • 類名(以及物件型別)始終以大寫字母開頭,

字母,這有助於將其與基本型別和變數名區分開來。

  • 你通常將一個類定義放在每個檔案中,並且名稱

檔案必須與類的名稱相同,字尾為.java。例如,Time類在名為Time.java的檔案中定義。

  • 在任何程式中,一個類都被指定為啟動

類。啟動類必須包含一個名為main的方法,程式的執行從此處開始。其他類可能有一個名為main的方法,但它們不會被執行。


解決了這些問題後,讓我們來看一個使用者定義型別的示例,Time。

建立新物件型別的常見動機是獲取幾個相關的資料片段並將它們封裝到一個物件中,該物件可以作為一個單元進行操作(作為引數傳遞,進行操作)。我們已經看到了兩個內建型別,例如Point和Rectangle。

另一個我們將自己實現的示例是Time,它用於記錄一天中的時間。構成時間的各種資訊是小時、分鐘和秒。因為每個Time物件都將包含這些資料,所以我們需要建立例項變數來儲存它們。

第一步是確定每個變數應該是什麼型別。小時和分鐘顯然應該是整數。為了使事情有趣,讓我們將秒設定為double,這樣我們就可以記錄秒的分數。

例項變數在類定義的開頭宣告,在任何方法定義之外,如下所示

class Time 
  int hour, minute;
  double second;

就其本身而言,此程式碼片段是一個合法的類定義。

宣告例項變數後,下一步通常是為新類定義一個建構函式。

建構函式

[編輯 | 編輯原始碼]

建構函式的通常作用是初始化例項變數。建構函式的語法類似於其他方法,但有三個例外

  • 建構函式的名稱與

類。

  • 建構函式沒有返回型別,也沒有返回值。
  • 省略關鍵字static。


以下是Time類的示例

 public Time () 
   this.hour = 0;
   this.minute = 0;
   this.second = 0.0;


請注意,在您期望看到返回型別的位置,在public和Time之間,什麼也沒有。這就是我們(以及編譯器)如何判斷這是一個建構函式的方式。

此建構函式不帶任何引數,如空括號()所示。建構函式的每一行都將一個例項變數初始化為一個任意的預設值(在本例中為午夜)。名稱this是一個特殊的關鍵字,是我們正在建立的物件的名稱。你可以像使用任何其他物件的名稱一樣使用this。例如,你可以讀取和寫入this的例項變數,並且可以將this作為引數傳遞給其他方法。

但是你不需要宣告this,也不需要使用new來建立它。事實上,你甚至不允許對其進行賦值!this由系統建立;你所要做的就是將其值儲存在其例項變數中。

編寫建構函式時的一個常見錯誤是在末尾放置return語句。抵制這種誘惑。

更多建構函式

[編輯 | 編輯原始碼]

建構函式可以過載,就像其他方法一樣,這意味著你可以提供多個具有不同引數的建構函式。Java透過將new命令的引數與建構函式的引數進行匹配來知道呼叫哪個建構函式。

通常有一個不帶引數的建構函式(如上所示),還有一個建構函式,其引數列表與例項變數列表相同。例如

 public Time (int hour, int minute, double second) 
   this.hour = hour;
   this.minute = minute;
   this.second = second;


引數的名稱和型別與例項變數的名稱和型別完全相同。建構函式所做的只是將資訊從引數複製到例項變數。

如果你返回並檢視Point和Rectangle的文件,你會發現這兩個類都提供了這樣的建構函式。過載建構函式提供了靈活性,可以先建立一個物件,然後填充空白,或者在建立物件之前收集所有資訊。

到目前為止,這可能看起來不是很有趣,事實上它確實不是。編寫建構函式是一個乏味且機械的過程。一旦你寫了兩個,你會發現你可以一邊睡覺一邊寫出來,只要看看例項變數列表就可以了。

建立新物件

[編輯 | 編輯原始碼]

儘管建構函式看起來像方法,但你永遠不會直接呼叫它們。相反,當你使用new命令時,系統會為新物件分配空間,然後呼叫你的建構函式來初始化例項變數。

以下程式演示了兩種建立和初始化Time物件的方法

 class Time 
   int hour, minute;
   double second;
 
   public Time () 
     this.hour = 0;
     this.minute = 0;
     this.second = 0.0;
 
   public Time (int hour, int minute, double second) 
     this.hour = hour;
     this.minute = minute;
     this.second = second;
   
 
   public static void main (String[] args) 
 
     // one way to create and initialize a Time object
     Time t1 = new Time ();
     t1.hour = 11;
     t1.minute = 8;
     t1.second = 3.14159;
     System.out.println (t1);
 
     // another way to do the same thing
     Time t2 = new Time (11, 8, 3.14159);
     System.out.println (t2);
   

作為練習,找出程式的執行流程。

在main中,我們第一次呼叫new命令時,我們沒有提供任何引數,因此Java呼叫第一個建構函式。接下來的幾行將值分配給每個例項變數。

我們第二次呼叫new命令時,我們提供的引數與第二個建構函式的引數匹配。這種初始化例項變數的方法更簡潔(效率也略高),但可能更難閱讀,因為它不清楚哪些值分配給哪些例項變數。

列印物件

[編輯 | 編輯原始碼]

此程式的輸出為

 Time@80cc7c0
 Time@80cc807

當 Java 列印使用者定義的物件型別的值時,它會列印型別的名稱和一個特殊的十六進位制(以 16 為基數)程式碼,該程式碼對於每個物件都是唯一的。此程式碼本身沒有意義;實際上,它可能因機器而異,甚至因執行而異。但它對於除錯很有用,以防您想跟蹤各個物件。

為了以對使用者(而不是程式設計師)更有意義的方式列印物件,通常需要編寫一個名為 printTime 之類的方法。

 public static void printTime (Time t) 
   System.out.println (t.hour + ":" + t.minute + ":" + t.second);


將此方法與“時間”部分中的 printTime 版本進行比較。

如果我們將 t1 或 t2 作為引數傳遞,則此方法的輸出為 11:8:3.14159。雖然這可以識別為時間,但它並非完全採用標準格式。例如,如果分鐘數或秒數小於 10,我們期望有一個前導 0 作為佔位符。此外,我們可能希望刪除秒的小數部分。換句話說,我們想要類似 11:08:03 的東西。

在大多數語言中,都有簡單的方法來控制數字的輸出格式。在 Java 中,沒有簡單的方法。

Java 提供了非常強大的工具來列印格式化內容(如時間和日期),以及解釋格式化輸入。不幸的是,這些工具不太容易使用,因此我將它們排除在本手冊之外。但是,如果您願意,可以檢視 java.util 包中 Date 類的文件。


物件操作

[編輯 | 編輯原始碼]

即使我們無法以最佳格式列印時間,我們仍然可以編寫操作 Time 物件的方法。在接下來的幾個部分中,我將演示操作物件的方法的幾種可能的介面。對於某些操作,您將可以選擇幾種可能的介面,因此您應該考慮每種介面的優缺點。

純函式:將物件和/或基本型別作為引數,但不修改物件。返回值要麼是基本型別,要麼是在方法內部建立的新物件。

修改器:將物件作為引數並修改其中一些或全部物件。通常返回 void。void

填充方法:引數之一是一個物件,該物件由方法填充。從技術上講,這是一種修改器。


純函式

[編輯 | 編輯原始碼]

如果結果僅取決於引數,並且沒有副作用(例如修改引數或列印內容),則該方法被認為是純函式。呼叫純函式的唯一結果是返回值。

一個例子是 after,它比較兩個 Time 並返回一個布林值,指示第一個運算元是否在第二個運算元之後。

 public static boolean after (Time time1, Time time2) 
   if (time1.hour > time2.hour) return true;
   if (time1.hour < time2.hour) return false;
   if (time1.minute > time2.minute) return true;
   if (time1.minute < time2.minute) return false;
   if (time1.second > time2.second) return true;
   return false;


如果兩個時間相等,此方法的結果是什麼?這似乎是此方法的合適結果嗎?如果您正在為該方法編寫文件,您是否會專門提及這種情況?

第二個例子是 addTime,它計算兩個時間的總和。例如,如果現在是 9:14:30,並且您的麵包機需要 3 小時 35 分鐘,則可以使用 addTime 計算麵包何時做好。

這是一個不完全正確的此方法的粗略草稿。

 public static Time addTime (Time t1, Time t2) 
   Time sum = new Time ();
   sum.hour = t1.hour + t2.hour;
   sum.minute = t1.minute + t2.minute;
   sum.second = t1.second + t2.second;
   return sum;

雖然此方法返回一個 Time 物件,但它不是建構函式。您應該返回並比較此類方法的語法與建構函式的語法,因為很容易混淆。

以下是如何使用此方法的示例。如果 currentTime 包含當前時間,breadTime 包含麵包機制作麵包所需的時間,則可以使用 addTime 計算麵包何時做好。

   Time currentTime = new Time (9, 14, 30.0);
   Time breadTime = new Time (3, 35, 0.0);
   Time doneTime = addTime (currentTime, breadTime);
   printTime (doneTime);

該程式的輸出為 12:49:30.0,這是正確的。另一方面,在某些情況下,結果不正確。你能想到一個嗎?

問題在於此方法沒有處理秒數或分鐘數加起來超過 60 的情況。在這種情況下,我們必須將多餘的秒“進位”到分鐘列,或將多餘的分鐘進位到小時列。

這是此方法的第二個已更正版本。

 public static Time addTime (Time t1, Time t2) 
   Time sum = new Time ();
   sum.hour = t1.hour + t2.hour;
   sum.minute = t1.minute + t2.minute;
   sum.second = t1.second + t2.second;
   if (sum.second >= 60.0) 
     sum.second -= 60.0;
     sum.minute += 1;
   
   if (sum.minute >= 60) 
     sum.minute -= 60;
     sum.hour += 1;
   
   return sum;


雖然它是正確的,但它開始變得龐大。稍後,我將建議解決此問題的另一種方法,該方法將更短。

此程式碼演示了我們之前從未見過的兩個運算子,+= 和 -=。這些運算子提供了一種簡潔的方法來遞增和遞減變數。它們類似於 ++ 和 --,但 (1) 它們適用於 double 和 int,以及 (2) 遞增量不必為 1。語句 sum.second -= 60.0; 等效於 sum.second = sum.second - 60;


修改器

[編輯 | 編輯原始碼]

作為修改器的示例,請考慮 increment,它將給定數量的秒新增到 Time 物件。同樣,此方法的粗略草稿如下所示

 public static void increment (Time time, double secs) 
   time.second += secs;
   if (time.second >= 60.0) 
     time.second -= 60.0;
     time.minute += 1;
   
   if (time.minute >= 60) 
     time.minute -= 60;
     time.hour += 1;


第一行執行基本操作;其餘部分處理我們之前看到的相同情況。

此方法是否正確?如果引數 secs 大於 60 會發生什麼?在這種情況下,僅減去 60 一次是不夠的;我們必須繼續這樣做,直到 second 小於 60。我們可以透過簡單地將 if 語句替換為 while 語句來做到這一點。

 public static void increment (Time time, double secs) 
   time.second += secs;
   while (time.second >= 60.0) 
     time.second -= 60.0;
     time.minute += 1;
   
   while (time.minute >= 60) 
     time.minute -= 60;
     time.hour += 1;


此解決方案是正確的,但效率不高。你能想到一個不需要迭代的解決方案嗎?


填充方法

[編輯 | 編輯原始碼]

有時您會看到像 addTime 這樣的方法使用不同的介面(不同的引數和返回值)。與其每次呼叫 addTime 時都建立一個新物件,我們可以要求呼叫方提供一個“空”物件,addTime 應該在其中儲存結果。將以下內容與先前版本進行比較。

 public static void addTimeFill (Time t1, Time t2, Time sum) 
   sum.hour = t1.hour + t2.hour;
   sum.minute = t1.minute + t2.minute;
   sum.second = t1.second + t2.second;
   if (sum.second >= 60.0) 
     sum.second -= 60.0;
     sum.minute += 1;
   
   if (sum.minute >= 60) 
     sum.minute -= 60;
     sum.hour += 1;


這種方法的一個優點是呼叫方可以選擇重複使用同一個物件來執行一系列加法。這可能稍微提高效率,儘管它可能令人困惑,以至於會導致細微的錯誤。對於絕大多數程式設計,值得花費一點執行時間來避免大量的除錯時間。


哪個最好?

[編輯 | 編輯原始碼]

任何可以使用修改器和填充方法完成的事情也可以使用純函式完成。實際上,有一些程式語言稱為函數語言程式設計語言,它們只允許純函式。一些程式設計師認為,使用純函式的程式比使用修改器的程式開發速度更快且錯誤更少。然而,有時修改器很方便,在某些情況下,函式式程式效率較低。

總的來說,我建議您在合理的情況下編寫純函式,並且僅在有令人信服的優勢時才使用修改器。這種方法可能被稱為函數語言程式設計風格。


增量開發與規劃

[編輯 | 編輯原始碼]

在本章中,我演示了一種我稱為“快速原型設計和迭代改進”的程式開發方法。在每種情況下,我都編寫了一個執行基本計算的粗略草稿(或原型),然後在一些情況下對其進行了測試,並在發現錯誤時對其進行了更正。

雖然這種方法可能有效,但它可能導致程式碼變得不必要地複雜(因為它處理了許多特殊情況)且不可靠(因為很難說服自己已經找到了所有錯誤)。

另一種方法是高階規劃,在這種方法中,對問題的深入瞭解可以使程式設計變得更加容易。在這種情況下,洞察力在於,時間實際上是以 60 為基數的三位數!秒是“個位數”,分鐘是“60 位數”,小時是“3600 位數”。

當我們編寫 addTime 和 increment 時,我們實際上是在進行以 60 為基數的加法,這就是為什麼我們必須將一個列“進位”到下一列的原因。

因此,解決整個問題的另一種方法是將 Time 轉換為 double,並利用計算機已經知道如何對 double 進行算術運算這一事實。這是一個將 Time 轉換為 double 的方法。

 public static double convertToSeconds (Time t) 
   int minutes = t.hour * 60 + t.minute;
   double seconds = minutes * 60 + t.second;
   return seconds;

現在我們只需要一種方法將 double 轉換為 Time 物件。我們可以編寫一個方法來執行此操作,但將其編寫為第三個建構函式可能更有意義。

 public Time (double secs) 
   this.hour = (int) (secs / 3600.0);
   secs -= this.hour * 3600.0;
   this.minute = (int) (secs / 60.0);
   secs -= this.minute * 60;
   this.second = secs;


此建構函式與其他建構函式略有不同,因為它涉及一些計算以及對例項變數的賦值。

您可能需要思考一下才能說服自己我用來在不同基數之間轉換的技術是正確的。假設您已經確信,我們可以使用這些方法重寫 addTime。

 public static Time addTime (Time t1, Time t2) 
   double seconds = convertToSeconds (t1) + convertToSeconds (t2);
   return new Time (seconds);


這比原始版本短得多,並且更容易證明它是正確的(假設,像往常一樣,它呼叫的方法是正確的)。作為練習,以相同的方式重寫 increment。


在某些方面,從以 60 為基數轉換為以 10 為基數再轉換回來比僅僅處理時間更難。基數轉換更抽象;我們處理時間的直覺更好。

但是,如果我們有將時間視為以 60 為基數的數字的洞察力,並投入編寫轉換方法(convertToSeconds 和第三個建構函式),我們就可以得到一個更短、更易於閱讀和除錯以及更可靠的程式。

以後也更容易新增更多功能。例如,想象一下減去兩個 Time 以找到它們之間的持續時間。天真的方法是實現減法,包括“借位”。使用轉換方法會容易得多。

具有諷刺意味的是,有時使問題變得更難(更通用)會使問題變得更容易(更少的特殊情況,更少的出錯機會)。


演算法

[編輯 | 編輯原始碼]

當你為一類問題編寫通用解決方案,而不是為單個問題編寫特定解決方案時,你就編寫了一個演算法。我在第一章中提到了這個詞,但沒有仔細定義它。它不容易定義,所以我將嘗試幾種方法。

首先,考慮一些不是演算法的東西。例如,當你學習乘以一位數時,你可能記住了乘法表。實際上,你記住了100個特定的解決方案,所以這些知識並不是真正的演算法。

但如果你很,你可能會透過學習一些技巧來作弊。例如,要找到 7 和 9 的乘積,你可以將 7 作為第一位數字,並將 2 作為第二位數字。這個技巧是將任何一位數乘以 9 的通用解決方案。這就是一個演算法!

類似地,你學習的帶進位的加法、帶借位的減法和長除法的技巧都是演算法。演算法的一個特點是它們不需要任何智慧就能執行。它們是機械過程,其中每一步都根據一組簡單的規則從上一步得出。

在我看來,人類在學校花費如此多的時間學習執行演算法,而這些演算法從字面上講不需要任何智力,這令人尷尬。

另一方面,設計算法的過程很有趣,具有智力挑戰性,並且是我們所說的程式設計的核心部分。

人們自然而然地、毫不費力地或無意識地做的一些事情,在演算法上表達起來是最困難的。理解自然語言就是一個很好的例子。我們都這樣做,但到目前為止,還沒有人能夠解釋我們是如何做到的,至少不是以演算法的形式。

稍後你將有機會為各種問題設計簡單的演算法。


術語表

[編輯 | 編輯原始碼]

類(class):之前,我將類定義為相關方法的集合。在本章中,我們瞭解到類定義也是一種新型物件的模板。

例項(instance):類的成員。每個物件都是某個類的例項。

構造器(constructor):一種特殊的初始化新構造物件的例項變數的方法。

專案(project):一個或多個類定義(每個檔案一個)的集合,構成一個程式。

啟動類(startup class):包含程式開始執行的 main 方法的類。

函式(function):一種其結果僅取決於其引數且除了返回值之外沒有副作用的方法。

函數語言程式設計風格(functional programming style):一種程式設計風格,其中大多數方法都是函式。

修改器(modifier):一種更改其作為引數接收的一個或多個物件的方法,通常返回 void。

填充方法(fill-in method):一種方法型別,它以一個物件作為引數,並填充其例項變數,而不是生成返回值。這種方法型別通常不是最佳選擇。

演算法(algorithm):一組透過機械過程解決一類問題的指令。

華夏公益教科書