2018年11月20日 星期二

Java SE 入門教學 - Object 類別的常見方法

更新時間:11/20/2018

前言

Object 類別是所有類別的根,也就是說當您自己撰寫任何類別的時候,都一定會繼承 Object 類別內的東西,這些東西還真是形影不離啊......。既然每個地方都可以看見它們的存在,勢必要好好「瞭解」與「利用」這些好~夥伴!

方法名稱 方法說明
protected Object clone() 複製此物件。
boolean equals(Object obj) 比較兩個物件是否相等。
protected void finalize() 如果物件沒有任何參考指向它,則會呼叫垃圾收集器(garbage collector)回收。
Class<?> getClass() 取得此物件執行時期的類別。
int hashCode() 雜湊值。
String toString() 回傳代表此物件的字串說明。
void notify() 喚醒正在此物件監視上等待的單個線程。
void notifyAll() 喚醒等待此物件監視上等待的所有線程。
void wait() 導致當前線程等待,直到另一個線程調用此物件的 notify() 方法或 notifyAll() 方法。
void wait(long timeout) 導致當前線程等待,直到另一個線程調用此物件的 notify() 方法或 notifyAll() 方法,或者已經過了指定的時間量。
void wait(long timeout, int nanos) 導致當前線程等待,直到另一個線程為此對象調用 notify() 方法或 notifyAll() 方法,或者某個其他線程中斷當前線程,或者已經過了指定的時間量。

一、Equals() 方法

1.1 為何要覆寫 equals() 方法

當一個類別有自己特有的「邏輯相等」概念(不同於物件身份的概念)。
因為:(i)封裝 (ii)資料結構
目的:比較兩個物件中的(私有)屬性變數值是否一樣。

預設的 equals 方法只有在兩個參考變數參考同一個物件(位址相同)時,才會傳回 true。

如果覆寫 equals() 方法,也必須同時覆寫 hashCode() 方法。

1.2 設計 equals() 方法

1.2.1) 使用 instanceof 操作符檢查「物件參考變數是否為正確的類型」。
1.2.2) 對於類別中的每一個「關鍵屬性變數」,檢查物件參考變數中的屬性變數與當前物件實例中對應的屬性變數值。
1.2.3) 對於不是 float 與 double 類型的基本型別(Primitive type),使用「==」比較。
1.2.4) 對於物件型別(Class type),使用 equals() 方法。
1.2.5) 對於 float 型別,使用 Float.floatToIntBits(afloat) 轉換為 int 型別,再使用「==」比較。
1.2.6) 對於 double 型別,使用 Double.doubleToLongBits(adouble) 轉換為 long 型別,再使用「==」比較。
1.2.7) 對於數組(Arrays),調用 Arrays.equals() 方法。

二、HashCode() 方法

2.1 為何要覆寫 hashCode() 方法

當改寫 equals() 的時候,總是要改寫 hashCode()。

用於驗證兩個物件實例中的屬性變數值是否相同,若有相同的雜湊值表示二個物件中的屬性變數值是相同的。

若 equals() 比較兩個物件中的屬性變數值一樣,則 Java 規定其 hashCode() 產生的雜湊值也必須相同;但,Java 允許兩個相同的物件,其雜湊值並不一樣。

2.2 設計 hashCode() 方法

在類別中對於關鍵屬性變數分別計算雜湊值,結果相加儲存到變數(result)中。

2.2.1) 把某個非零常整數值,最好是「質數」,例如 7,保存在變數 result 中。
2.2.2) 再找另一個質數,例如 83。
對於類別中的每一個「關鍵屬性變數」(指 equals() 方法中考慮的每一個屬性變數),根據 2.2.3 ~ 2.2.10 的規則分別計算後的值再與 83*result 相加
2.2.3) 對於 boolean 型別,計算(value ? 0 : 1)。
2.2.4) 對於 byte, char, short 型別,計算(int)。
2.2.5) 對於 int 型別,直接取值。
2.2.6) 對於 long 型別,計算(int)(value ^ (value>>>32))。
2.2.7) 對於 float 型別,計算 Float.floatToIntBits(afloat)。
2.2.8) 對於 double 型別,計算 Double.doubleToLongBits(adouble)得到一個 long 型別的值,再使用 2.2.5) 規則。
2.2.9) 對於物件型別(Class type),使用 hashCode() 方法。
2.2.10) 對於數組(Arrays),對其中每個元素調用它的 hashCode() 方法。
2.2.11) 返回 result。

三、toString() 與 finalize() 方法

這兩個方法在之前的文章中有說明,請參閱 認識 Object 類別


這邊我們先使用一個範例測試 equals() 和 hashCode()。


四、clone() 方法

對於克隆(Clone),Java 有一些限制:

4.1 被克隆的類別必須自己實現 Cloneable 介面,以指示 Object.clone() 方法可以合法地對該物件實例進行按字段複製。 Cloneable 介面實際上是個標識介面,沒有任何介面方法。

4.2 實現 Cloneable 介面的類別應該使用公共方法重寫 Object.clone(它是 protected)。

4.3 在 Java.lang.Object 類別中克隆方法是這麼定義的:

protected Object clone() throws CloneNotSupportedException {
  if (!(this instanceof Cloneable)) {
    throw new CloneNotSupportedException (
        "Class " + getClass().getName() +
        " doesn't implement Cloneable"
       );
  }

  return internalClone();
}

/*
 * Native helper method for cloning.
 */
private native Object internalClone();

創建並返回此對象的一個副本。
可以看到,它的實現非常的簡單,它限制所有調用 clone() 方法的對象,都必須實現 Cloneable 接口,否者將拋出 CloneNotSupportedException 這個異常。最終會調用 internalClone() 方法來完成具體的操作。而 internalClone() 方法,實則是一個 native 的方法。對此我們就沒必要深究了,只需要知道它可以 clone() 一個對象得到一個新的對象實例即可。

4.4 實作 Cloneable 介面

按照慣例,返回的物件實例應該通過調用 super.clone 獲得。

淺拷貝(Shallow Copy):指的是你的物件實例本身被拷貝,而沒有拷貝物件實例內的物件參考的屬性變數。

淺拷貝(Shallow Copy):對基本型態進行值的傳遞
對類別型態(物件參考變數)指向原地址

深拷貝(Deep Copy):指的是包含物件實例本身和物件參考的屬性變數的拷貝。

深拷貝(Deep Copy):對基本型態進行值的傳遞
對類別型態(物件參考變數)創建一個新的實例 (新地址),並複製其內容

簡單點說:就是淺拷貝的兩個物件實例中的物件參考的屬性變數還會指向同一個地址,而深拷貝則全部單獨了。也就是說深拷貝把關聯關係也拷貝了。

範例


五、getClass() 方法

只要是「類別型態」就可以利用這個方法,就可以獲得一個在執行中物件實例的類別型態。

例如,使用字串 "Java is nice.".getClass() 即可得到 java.lang.String 類別。

您會發現原來陣列 float[] 和 byte[][] 居然是個類別!!!在 Java 中,陣列確實是會被轉換成類別 [F[[B

如果把第16行註解拿掉,會出現編譯錯誤。

TestGetClass.java:16: error: int cannot be dereferenced
                Class c4 = num.getClass();
                              ^
TestGetClass.java:16: error: incompatible types:  cannot be converted to Class
                Class c4 = num.getClass();
                                       ^
2 errors

六、wait() / notify() / notifyAll() 方法

多線程之間協調工作

例如,瀏覽器顯示圖片的線程 displayThread 想要執行顯示圖片的任務,必須等待下載線程 downloadThread 將該圖片下載完畢。如果圖片還沒有下載完,displayThread 可以暫停,當 downloadThread 完成了任務後,再通知 displayThread「圖片準備完畢,可以顯示了」,這時,displayThread 接續執行。

以上邏輯簡單的說就是:如果條件不滿足,則等待。當條件滿足時,等待該條件的線程將被喚醒。在 Java 中,這個機制的實現依賴於 wait/notify 等待機制與鎖機制是密切關聯的。例如:

當線程 A 獲得了obj 鎖後,發現條件 condition 不滿足,無法繼續處理,於是線程 A 就 wait()。

synchronized(obj) {

  while(!condition) {

    obj.wait();

  }

  obj.doSomething();

}

在另一線程 B 中,如果 B 更改了某些條件,使得線程 A 的 condition 條件滿足了,就可以喚醒線程 A。

synchronized(obj) {
  
  condition = true;

  obj.notify();

}

需要注意的概念是:

◆ 調用 obj 的 wait()、notify() 方法前,必須獲得 obj 鎖,也就是必須寫在 synchronized(obj) {……} 代碼段內。

◆ 調用 obj.wait() 後,線程 A 就釋放了 obj 的鎖,否則線程 B 無法獲得 obj 鎖,也就無法在 synchronized(obj) {……} 代碼段內喚醒 A。

◆ 當 obj.wait() 方法返回後,線程 A 需要再次獲得 obj 鎖,才能繼續執行。

◆ 如果 A1、A2、A3 都在 obj.wait(),則 B 調用 obj.notify() 只能喚醒 A1、A2、A3 中的一個 (具體哪一個由 JVM 決定)。

◆ obj.notifyAll() 則能全部喚醒 A1、A2、A3,但是要繼續執行 obj.wait() 的下一條語句,必須獲得 obj 鎖,因此,A1、A2、A3 只有一個有機會獲得鎖繼續執行,例如 A1,其餘的需要等待 A1 釋放 obj 鎖之後才能繼續執行。

◆ 當 B 調用 obj.notify/notifyAll 的時候,B 正持有 obj 鎖,因此,A1、A2、A3 雖被喚醒,但是仍無法獲得 obj 鎖。直到 B 退出 synchronized 塊,釋放 obj 鎖後,A1、A2、A3 中的一個才有機會獲得鎖繼續執行。

詳細概念可搭配多線程與之對照。


七、總結

要覆寫 equals() 時,一併要覆寫 hashCode(),其關鍵屬性變數計算方式有其獨特規則。

要描述一個類別時,覆寫 toString() 使用文本描述。

物件實例要回收時,消失前還想要處理或交代事宜,請交給 finalize()。

覆寫 clone() 時,此類別必須實作 Cloneable 介面,並調用 super.clone() 方法。

多線程的管理使用 wait() / notify() / notifyAll() 相輔相成,並在線程內使用 鎖、同步 synchronized(param ...){...} 實現「等待機制」與「鎖機制」。





沒有留言:

張貼留言