JDK原始碼閱讀-Object類

七印miss發表於2019-01-12

概述

Object類是類層次結構的根類,可以作為各種的通用持有者。它是每個java類的基類,如果沒有明確指出基類,Object就被認為是當前定義的類的基類。包括arrays在內的所有物件,都實現Object類的方法。Object類屬於java.lang包,在這個類中有很多native(本地)方法。其類圖如下(JDK8):

Object類圖

構造方法

Object類沒有顯示的構造方法,只有編譯器預設提供的構造方法。

欄位

Object沒有欄位。

方法

Object只包含12個方法,但這些方法都十分重要。
訪問限制級別可分為:

  • public: equals(Object), hashCode(), notify(), notifyAll(), toString(), wait(), wait(long), wait(long, int)
  • protected: clone(), finalize()
  • private: registerNatives()

是否為final可分為:

  • final: getClass(), notify(), notifyAll(), wait(), wait(long), wait(long, int)registerNatives()(類私有方法自動成為final)。這些方法不能被子類重寫。
  • 非final: hashCode(), equals(Object), clone(), toString(), finalize()。這些方法可以被子類重寫,且必須滿足通用的約定,否則其他依賴於這些約定的類就無法與該類正常工作。

是否為native可分為:

  • native:registerNatives(), getClass(), hashCode(), clone(), notify(), notifyAll(), wait(long)。這些方法是用C/C++在動態庫中實現的,然後通過JNI(java Native Inteface)呼叫。Java語言本身不能對作業系統底層進行訪問與操作,但可以通過JNI介面來呼叫其他語言來實現對底層的訪問,JNI已加入Java標準。
  • 非native:equals(Object), toString(), wait(), wait(long, int)finalize()

registerNatives()方法

其主要作用是將C/C++中的方法對映到Java中的native方法,實現方法命名的解耦。在類初始化時呼叫static塊,執行registerNatives方法。

private static native void registerNatives();
static {
    registerNatives();
}
複製程式碼

getClass()方法

返回包含物件資訊的類物件(Class型別的例項),並且返回的類物件是被此類的靜態同步(static synchronized)方法鎖定的**(啥意思?)**。型別類Class表示一個型別的類,因為一切皆物件,型別也不例外,因此所有的型別類都是Class類的例項。

public final native Class<?> getClass();
複製程式碼

與執行緒有關的方法

包括wait,notifynotifyAll,暫不分析,先佔個坑,之後補上。

finalize()方法

類似C++的解構函式,當GC(Garbage Collector)確定不存在對該物件的更多引用時,由物件的GC呼叫此方法,用於釋放資源。一般不建議使用此方法來釋放非記憶體資源。它不同於C++的解構函式,解構函式在物件作用域呼叫,執行時間點確定。而此方法,是在記憶體不足,GC發生時進行呼叫,由於GC是不確定隨機的,所以無法確定此方法的執行時間。

protected void finalize() throws Throwable {}
複製程式碼

equals(Object)方法

equals方法用於兩個物件是否相等。Object的equals方法很簡單,只用兩個物件的引用是否相等來判斷是否是同一個物件。Java規範要求equals方法具備以下特性:

  1. 自反性:對於任何非空引用,x.equals(x)應該返回true;
  2. 對稱性:對於任何引用x和y,當且僅當y.equals(x)返回true,x.equals(y)也應該返回true;
  3. 傳遞性:對於任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也應該返回true;
  4. 一致性:如果x和y引用的物件沒有發生變化,反覆呼叫x.equals(y)應該返回同樣的結果;
  5. 對於任意非空引用x,x.equals(null)應該返回false。

Object類的equals實現為:

public boolean equals(Object obj) {
    return (this == obj); // 大佬都不嫌麻煩用括號擴起,習慣使然!
}
複製程式碼

然而在大多數情況下,只將引用作為判斷物件的唯一標準,太嚴格且沒有實際意義。例如對於大多數實體類,如Book類,我們對比兩個Book類例項時,關注的是這兩個例項的狀態(書名、價格、作者等)是否相等。如果兩者的狀態都相等,即使不是同一個引用,我們也判定這兩個Book例項相等。下面實現一個判斷兩個Book例項是否相等的equals方法:

public class Book {
    private String name; // 書名
    private int price; // 價格
    private String writer; // 作者

    @Override // 1. 帶註解,防止入參型別錯誤
    public boolean equals(Object otherObject){
        // 2. 檢測兩個例項引用是否相同,優化需要。
        if (this == otherObject){
            return true;
        }

        // 3. 檢測被比較物件是否為空,必須
        if (otherObject == null){
            return false;
        }

        // 4. 比較this和入參是否同屬於一個類
        if (getClass() != otherObject.getClass()){
            return false;
        }

        // 5. 將otherObject轉換為相應的類型別變數,為後續比較具體域狀態做準備
        Book other = (Book) otherObject;

        // 6. 比較所有的域。有物件域可能都為null,因此不能使用name.equals(other.name)這種方法
        return Objects.equals(name, other.name)
                && price == other.price
                && Objects.equals(writer, other.writer);
    }
}
複製程式碼

在程式碼的第4步中,使用getClass()來判斷兩個物件是否同屬於一個類,其使用場景為:equals的定義在每個子類中有所改變。如果所有的子類都擁有統一的語義,如所有的子類都使用父類的equals方法,則使用instanceof代替getClass()

if (!(otherObject instanceof Book)) {
    return false;
}
複製程式碼

equals在使用時,要格外注意當父類實現了equals方法,子類之間equals方法的實現方式要滿足對稱性的要求。
另外值得注意的是,如果重寫equals方法,則必須同時重新定義hashCode()方法,以便使用者可以將物件插入到雜湊表中。

hashCode()方法

Object類中的hashCode方法是native方法,返回一個整型數值(也可以是負數),無具體的實現方式。由於該方法是在Object中定義的,因此java中每個物件都會有一個預設的雜湊碼(hash code),其值可能是物件的儲存地址。

public native int hashCode();
複製程式碼

hashCode方法具體分析考慮之後結合集合再整理,先佔個坑。TODO...

toString()方法

Object的該方法返回了類的名稱加上'@',再加上此類雜湊碼的16進位制表示,如:I[@1a46e30,其中I[表示一個整型陣列。

public String toString(){
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
複製程式碼

一個好的習慣,應該是為每個類,特別是模型類重寫一個toString()方法,以便使用者能夠通過這個方法獲得物件狀態的必要資訊。一個反面例子就是陣列沒有重寫toString(),而是直接繼承了Object的toString()方法,因此陣列物件呼叫toString()方法,列印的字串都是類似I[@1a46e30這種形式的,雞肋!所以無奈只能使用靜態方法Arrays.toString(arr)或者Arrays.deepToString()方法。

絕對多數的toString方法都遵循這樣的格式:類的名字,隨後是一對方括號括起來的閾值。

下面是Person類中toString方法的實現:

public String toString() {
    return getClass().getName()
        + "[name=" + name
        + ", age=" + age
        + "]";
}
複製程式碼

toString在專案被頻繁使用,特別是只要物件與一個字串通過操作符"+"連線起來,或作為System.out.println()的入參,則編譯器都會預設呼叫物件的toString方法。

待續...

相關文章