類、物件和型別
| 導航 語言基礎 主題: |
一個 **物件** 由 **欄位** 和 **方法** 組成。欄位也稱為 *資料成員*、*特徵*、*屬性* 或 *特性*,描述了物件的 狀態。方法通常描述與特定物件相關的操作。可以將物件視為名詞,其欄位為描述該名詞的形容詞,其方法為該名詞可以執行的動詞。
例如,跑車是一個物件。它的某些欄位可能是它的高度、重量、加速度和速度。物件的欄位只是儲存有關該物件的資料。跑車的一些方法可能是 "駕駛"、"停車"、"比賽" 等。除非與跑車相關,否則方法並沒有多大意義,欄位也是如此。
讓我們構建跑車物件的藍圖稱為 *類*。類不會告訴我們跑車的速度有多快,或者它是什麼顏色,但它確實告訴我們我們的跑車將有一個代表速度和顏色的欄位,它們分別是數字和單詞(或十六進位制顏色程式碼)。類還為我們列出了方法,告訴汽車如何停車和駕駛,但這些方法僅靠藍圖無法執行任何操作——它們需要一個物件才能產生影響。
在 Java 中,類位於與自身名稱類似的檔案中。如果你想有一個名為 SportsCar 的類,它的原始檔需要是 SportsCar.java。透過在原始檔中放置以下內容來建立類:
程式碼清單 3.13: SportsCar.java
public class SportsCar {
/* Insert your code here */
}
|
該類目前還沒有做任何事情,因為你需要先新增方法和欄位變數。
物件不同於基本型別,因為
- 基本型別不會被例項化。
- 在記憶體中,對於基本型別,只儲存其值。對於物件,還可以儲存對例項的引用。
- 在記憶體中,基本型別的分配空間是固定的,無論其值如何。物件的分配空間可以變化,例如物件是否被例項化。
- 基本型別沒有可呼叫的方法。
- 基本型別不能被繼承。
為了從類到物件,我們透過 *例項化* 來 "構建" 我們的物件。例項化僅僅意味著建立類的 *例項*。例項和物件是非常相似的術語,有時可以互換使用,但請記住,例項指的是 *特定物件*,它是從類建立的。
這種例項化是由類的其中一個方法帶來的,稱為 *建構函式*。顧名思義,建構函式根據藍圖構建物件。在幕後,這意味著為例項分配計算機記憶體,並將值分配給資料成員。
一般來說,有四種類型的建構函式:預設、非預設、複製和克隆。
**預設建構函式** 將構建最基本的例項。通常,這意味著將所有欄位分配像 null、零或空字串這樣的值。但是,沒有什麼可以阻止你將預設跑車的顏色設定為紅色,但這通常是不好的程式設計風格。如果你的基本汽車是紅色而不是無色,另一個程式設計師會感到困惑。
程式碼部分 3.79:一個預設建構函式。
SportsCar car = new SportsCar();
|
**非預設建構函式** 用於建立具有為大多數(如果不是全部)物件的欄位指定了值的例項。汽車是紅色的,從 0-60 加速需要 12 秒,最高時速為 190 英里/小時,等等。
程式碼部分 3.80:一個非預設建構函式。
SportsCar car = new SportsCar("red", 12, 190);
|
**複製建構函式** 不包含在 Java 語言中,但是可以輕鬆地建立一個與複製建構函式相同功能的建構函式。重要的是要理解它是什麼。顧名思義,複製建構函式建立一個新的例項,作為已經存在的例項的副本。在 Java 中,這也可以透過使用預設建構函式建立例項,然後使用賦值運算子使它們等價來實現。但這在所有語言中都不可能,所以只要記住這個術語就可以了。
Java 具有 **克隆物件** 的概念,其最終結果與複製建構函式類似。克隆物件比使用 new 關鍵字建立更快,因為所有物件記憶體都會一次性複製到目標克隆物件中。這可以透過實現 Cloneable 介面來實現,該介面允許 Object.clone() 方法執行逐欄位複製。
程式碼部分 3.81:克隆物件。
SportsCar car = oldCar.clone();
|
當建立物件時,還會建立對該物件的引用。在 Java 中不能直接訪問物件,只能透過該物件引用訪問。該物件引用被分配了 *型別*。當將物件引用作為引數傳遞給方法時,我們需要該型別。Java 進行強型別檢查。
型別基本上是可以透過該物件引用執行的功能/操作列表。物件引用型別基本上是一個契約,保證這些操作在執行時存在。
當建立汽車時,它會附帶使用者手冊中列出的功能/操作列表,保證在使用汽車時這些功能/操作存在。
當你從類建立物件時,預設情況下它的型別與其類相同。這意味著類定義的所有功能/操作都存在並可用,並且可以使用。請參見以下內容:
程式碼部分 3.82:預設型別。
(new ClassName()).operations();
|
你可以將其分配給與類具有相同型別的變數:
程式碼部分 3.83:與類具有相同型別的變數。
ClassName objRefVariable = new ClassName();
objRefVariable.operations();
|
你可以將建立的物件引用分配給類、超類或類實現的介面:
程式碼部分 3.84:使用超類。
SuperClass objectRef = new ClassName(); // features/operations list are defined by the SuperClass class
...
Interface inter = new ClassName(); // features/operations list are defined by the interface
|
在汽車類比中,建立的汽車可能具有不同型別的駕駛員。我們為他們建立單獨的使用者手冊,一個普通使用者手冊、一個高階使用者手冊、一個兒童使用者手冊或一個殘疾人使用者手冊。每種型別的使用者手冊只描述適合該型別駕駛員的功能/操作。例如,高階駕駛員可能擁有其他檔位來切換到更高的速度,而其他型別的使用者則沒有……
當汽車鑰匙從成年人手中傳遞給兒童時,我們是在更換使用者手冊,這稱為 *型別轉換*。
在 Java 中,轉換可以透過三種方式發生:
- 向上轉換,沿著繼承樹向上,直到我們到達
Object - 向上轉換到類實現的介面
- 向下轉換,直到我們到達建立物件的類
自動裝箱和拆箱是 Java 1.5 之後引入的語言功能,在處理基本型別包裝器型別時,可以極大地簡化程式設計師的工作。請考慮以下程式碼片段:
程式碼部分 3.85:傳統的物件建立。
int age = 23;
Integer ageObject = new Integer(age);
|
基本型別包裝器物件是 Java 允許人們將基本資料型別視為物件的方式。因此,人們需要像上面展示的那樣,將自己的基本資料型別 *包裝* 到相應的基本型別包裝器物件中。
從 Java 1.5 開始,你可以像下面這樣編寫,編譯器會自動建立包裝物件。不再需要額外包裝基本型別。它已被 *自動裝箱* 到您的 behalf
程式碼部分 3.86:自動裝箱。
int age = 23;
Integer ageObject = age;
|
| 請記住,編譯器仍然會建立缺少的包裝器程式碼,因此在效能方面並沒有真正獲得任何好處。可以將此功能視為程式設計師的便利,而不是效能提升。 |
每個基本型別都有一個類包裝器:
| 基本型別 | 類包裝器 |
byte |
java.lang.Byte
|
char |
java.lang.Character
|
short |
java.lang.Short
|
int |
java.lang.Integer
|
long |
java.lang.Long
|
float |
java.lang.Float
|
double |
java.lang.Double
|
boolean |
java.lang.Boolean
|
void |
java.lang.Void
|
拆箱使用與裝箱相反的過程。花點時間研究一下以下程式碼。if 語句需要一個 boolean 原生值,但它被賦予了一個 Boolean 包裝物件。沒問題!Java 1.5 將自動拆箱此物件。
程式碼部分 3.87:拆箱。
Boolean canMove = new Boolean(true);
if (canMove) {
System.out.println("This code is legal in Java 1.5");
}
|
問題 3.11:考慮以下程式碼
問題 3.11:自動裝箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
這段程式碼中包含多少個自動裝箱和拆箱操作?
答案 3.11:自動裝箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
3
- 第 1 行有一個自動裝箱操作,用於賦值。
- 第 2 行有一個拆箱操作,用於進行加法運算。
- 第 2 行有一個自動裝箱操作,用於賦值。
- 第 3 行沒有自動裝箱或拆箱操作,因為
println()方法支援Integer類作為引數。
java.lang.Object 類中的方法是繼承的,因此所有類共享這些方法。
java.lang.Object.clone() 方法返回一個新物件,它是當前物件的副本。類必須實現標記介面 java.lang.Cloneable 以指示它們可以被克隆。
java.lang.Object.equals(java.lang.Object) 方法將物件與另一個物件進行比較,並返回一個 boolean 結果,指示這兩個物件是否相等。從語義上講,此方法比較物件的內容,而等式比較運算子 "==" 比較物件引用。equals 方法被 java.util 包中的許多資料結構類使用。這些資料結構類中的一些還依賴於 Object.hashCode 方法 - 請參閱 hashCode 方法瞭解有關 equals 和 hashCode 之間契約的詳細資訊。實現 equals() 不像看起來那樣簡單,請參閱 'equals() 的秘密' 獲取更多資訊。
java.lang.Object.finalize() 方法在垃圾收集器釋放物件記憶體之前恰好呼叫一次。類覆蓋 finalize 以執行在回收物件之前必須執行的任何清理操作。大多數物件不需要覆蓋 finalize。
無法保證何時呼叫 finalize 方法,也無法保證為多個物件呼叫 finalize 方法的順序。如果 JVM 在執行垃圾收集之前退出,作業系統可能會釋放物件,在這種情況下,finalize 方法不會被呼叫。
finalize 方法應始終被宣告為 protected 以防止其他類呼叫 finalize 方法。
protected void finalize() throws Throwable { ... }
java.lang.Object.getClass() 方法返回用於例項化物件的類的 java.lang.Class 物件。類物件是 Java 中 反射 的基類。java.lang.reflect 包中提供了額外的反射支援。
java.lang.Object.hashCode() 方法返回一個整數 (int)。雖然不完全,但可以使用此整數來區分物件。它可以快速分離大多數物件,具有相同雜湊碼的物件將在之後以其他方式分離。它被提供關聯陣列的類使用,例如,實現 java.util.Map 介面的類。它們使用雜湊碼將物件儲存在關聯陣列中。良好的 hashCode 實現將返回一個雜湊碼
- 穩定:不改變
- 均勻分佈:不相等物件的雜湊碼傾向於不相等,並且雜湊碼在整數值上均勻分佈。
第二點意味著兩個不同的物件可以具有相同的雜湊碼,因此具有相同雜湊碼的兩個物件不一定相同!
由於關聯陣列依賴於 equals 和 hashCode 方法,因此這兩個方法之間有一個重要的契約,如果物件要插入到 Map 中,則必須維護此契約。
- 對於兩個物件a 和b
a.equals(b) == b.equals(a)- 如果
a.equals(b),則a.hashCode() == b.hashCode() - 但
如果a.hashCode() == b.hashCode(),則a.equals(b)
為了維護此契約,覆蓋 equals 方法的類也必須覆蓋 hashCode 方法,反之亦然,以便 hashCode 基於與 equals 相同的屬性(或屬性的子集)。
地圖與物件之間的另一個契約是,一旦物件被插入到地圖中,hashCode 和 equals 方法的結果就不會改變。出於這個原因,通常最好將雜湊函式基於物件的不可變屬性。
java.lang.Object.toString() 方法返回一個 java.lang.String,其中包含物件的文字表示形式。當物件運算元與字串連線運算子 (+ 和 +=) 一起使用時,編譯器會隱式呼叫 toString 方法。
每個物件都有兩個與之關聯的執行緒等待列表。一個等待列表由 synchronized 關鍵字用於獲取與物件關聯的互斥鎖。如果互斥鎖當前由另一個執行緒持有,則當前執行緒將被新增到等待互斥鎖的阻塞執行緒列表中。另一個等待列表用於透過 wait 和 notify 以及 notifyAll 方法完成的執行緒之間的訊號傳遞。
使用 wait/notify 允許線上程之間有效地協調任務。當一個執行緒需要等待另一個執行緒完成操作,或者需要等待事件發生時,執行緒可以暫停其執行並等待事件發生時通知。這與輪詢形成對比,在輪詢中,執行緒會反覆休眠一小段時間,然後檢查標誌或其他條件指示器。輪詢既更耗費計算資源(因為執行緒必須繼續檢查),響應速度也更慢(因為執行緒直到下次檢查時才會注意到條件已改變)。
wait 方法有三個過載版本,用於支援以不同方式指定超時值:java.lang.Object.wait()、java.lang.Object.wait(long) 和 java.lang.Object.wait(long, int)。第一個方法使用超時值為零 (0),這意味著等待不會超時;第二個方法將毫秒數作為超時;第三個方法將納秒數作為超時,計算為 1000000 * timeout + nanos。
呼叫 wait 的執行緒被阻塞(從可執行執行緒集中刪除)並新增到物件的等待列表中。執行緒將一直保留在物件的等待列表中,直到發生以下三種事件之一
- 另一個執行緒呼叫物件的
notify或notifyAll方法; - 另一個執行緒呼叫執行緒的
java.lang.Thread.interrupt方法;或者 - 在對
wait的呼叫中指定的非零超時過期。
wait 方法必須在對物件進行同步的塊或方法中呼叫。這確保了 wait 和 notify 之間沒有競爭條件。當執行緒被放入等待列表時,執行緒會釋放物件的互斥鎖。線上程從等待列表中移除並新增到可執行執行緒集中之後,它必須在繼續執行之前獲取物件的互斥鎖。
java.lang.Object.notify() 和 java.lang.Object.notifyAll() 方法會從物件的等待列表中移除一個或多個執行緒,並將它們新增到可執行執行緒集中。notify 從等待列表中移除單個執行緒,而 notifyAll 則移除等待列表中的所有執行緒。notify 移除哪個執行緒是未指定的,取決於 JVM 實現。
notify 方法必須在對物件同步的塊或方法中呼叫。這可以確保在 wait 和 notify 之間不存在競爭條件。