博客 / 詳情

返回

Object,所有類的基類

Java 是一門典型的面嚮對象語言,提供 extends 關鍵字使子類繼承父類。

public class Student extends Person {
    ...
}

但是創建 Person 類時不用使用 extends 繼承 Object類。

public class Person extends Object {
    ...
}

因為,創建的類沒有明確指明繼承關係時,會在編譯時自動繼承 Object 類。可以使用 Object 類型的變量引用任何類型的實例:

Object sample = new Student(1001, "小咖", 20, "男");

也可以將任意的 Object 實例轉換為需要的類型:

Person p = (Person) sample;

上面兩個例子在編譯器中是不會報錯的。因此,Object 類是 Person 類的父類,每個類都是由 Object 擴展而來的。所以,熟悉 Object 類中提供的服務是十分重要的。

Java 中的基本類型不是對象,但也提供了相應的包裝類型,如 int 基本類型對應 Integer 包裝類型。但使用基本類型創建的數組是擴展自 Object 類,實際上是引用。

Object sample = new int[10];

其實,所有的數組類型都是擴展了 Object 類。

下表為 Object 類的通用方法。

方法 描述 異常
final native Class<?> getClass() 返回對象的運行時類
native int hashCode() 返回對象的散列碼
boolean equals(Object obj) 與其它對象是否相等
native Object clone() 克隆並返回對象的副本 CloneNotSupportedException
String toString() 返回對象的字符串表示
final native void notify() 喚醒正在等待對象監聽器上的一個線程
final native void notifyAll() 喚醒正在等待對象監聽器上的所有線程
final native void wait() 導致當前線程等待,直到另一個線程調用此對象的notify()notifyAll() InterruptedException
final native void wait(long timeout) 導致當前線程等待,直到另一個線程調用此對象的notify()notifyAll(),或者指定時間已到 InterruptedException
final void wait(long timeout, int nanos) 導致當前線程等待,直到另一個線程調用此對象的notify()notifyAll(),或者指定時間已到 InterruptedException
void finalize() 當GC確定不再有對該對象的引用時,由對象的 GC 調用此方法 Throwable

equals()

Object 類提供了 equals() 方法用於檢測對象是否相等。其實現相等性要滿足五個條件:

  1. 自反性。 對於任何的非空引用都需滿足 x.equals(x) == true
  2. 對稱性。 對於任何引用都滿足 x.equals(y) == y.equals(x)
  3. 傳遞性。 對於任何引用都滿足 (x.equals(y) && y.equals(z)) == x.equals(z)
  4. 一致性。 對於不發生任何變化的引用都滿足 x.equals(y) == x.equals(y),多次調用 equals() 方法結果不變。
  5. null 的比較。x.equals(null) 返回 false。對任何不是 null 的對象調用 equals() 方法與 null 比較的結果都為 false

引用類型最好使用 equals() 方法比較;而基本類型使用 == 比較。

其中,子類中定義 equals() 方法時,需先比較超類的 equals()。如果檢測失敗,對象就不可能相等。如果超類中的域都相等,就需要比較子類中的實例域。

下面可以從兩個截然不同的情況看一下這個問題:

  • 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用 getClass 進行檢測。
  • 如果由超類決定相等的概念,那麼就可以使用 instanceof 進行檢測,這樣可以在不同子類的對象之間進行相同的比較。

對象中使用 equals() 方法比較需要實現的步驟如下:

  1. 檢查是否為同一個引用,如果是直接返回 true
  2. 檢測傳入的值是否為 null,如果是直接返回 false
  3. 檢測是否屬於同一個類型。如果不是直接返回 false
  4. Object 對象類型轉為要比較的類型。
  5. 比較每個關鍵域是否相等。
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o; 
    return Objects.equals(name, person.name) 
            && Objects.equals(sex, person.sex)
            && (age != person.age);
}

如果子類重新定義 equals,就要在其中包含 super.equals(other)

hasCode()

hashCode() 返回的是整數值,是無規律的散列碼。hashCode() 定義在了 Object 類中,每個類都可以使用 hashCode()方法調用自身散列碼,其值為對象的存儲地址。

Object sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // [I@5b464ce8

如果你在創建的類中覆蓋了 equals() 方法,就必須覆蓋 hashCode() 方法 。這是 hashCode() 的通用約定。下面是覆蓋 hashCode() 方法的約定:

  • 程序執行期間,對象的 equals() 方法中比較的信息不變,同一個對象的 hashCode() 方法的返回值也不變。兩個程序的執行期間,hashCode() 方法返回的值可以不一致。
  • 如果兩個對象根據 equals(Object) 方法比較相等,那 hashCode() 方法的返回值也必須相等。
  • 如果兩個對象根據 equals(Object) 方法比較不相等,那 hashCode() 方法的返回值最好不相等。如果相等,會在使用 Map 時造成散列碼衝突。

總結就是兩個對象相等,其散列碼一定相同;但是散列碼相同的兩個對象並不一定相等。因為計算散列碼具有隨機性,兩個值不同的對象可能計算出相同的散列碼。

理想的散列函數是把集合中不相等的實例均勻地分佈到所有可能的 int 之上。但非常困難,只能實現相對接近這種理想的情形。

當計算散列碼時,要將每個域都考慮進去。可以將每個域都當成 R 進制的某一位,然後組成一個 R 進制的整數。

R 一般取奇數 31,偶數會出現乘法溢出,信息會丟失。因為與 2 相乘相當於向左移一位,最左邊的位丟失。並且一個數與 31 相乘可以轉換成移位和減法:31*x == (x<<5)-x,編譯器會自動進行這個優化。

如下是一個如何簡單的計算散列碼的參考:

  • 基本類型調用 Type.hashCode(value) 來生成。Type 類型為基本類型各自的包裝類。
  • 如果是引用類型,並且需要覆蓋 equals() 方法,equals() 方法使用哪些域比較,hashCode() 方法也會遞歸地調用這些域的散列碼並計算。
  • 如果是數組類型,那數組中的每個值都當做單獨的域來處理。也可以使用 Arrays.hashCode() 方法計算。

不要試圖從散列碼計算中排除掉一個對象的關鍵域來提高性能

下面重寫 Student 類的 hashCode() 方法:

@Override
public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + sid;
    return result;
}

hashCode() 方法返回的散列碼也可以是負數,合理地組合實例域的散列碼,以便能夠讓各個不同的對象產生的散列碼更加均勻。

toString()

默認的 toString() 方法是返回的是 java.lang.Object@511baa65 這種類型是的字符串。

Student sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // java.lang.Object@511baa65

上面 System.out.println(sample) 會自動調用 sample.toString() 方法將值輸出到控制枱。但是,提供好的 toString() 實現可以獲取對象狀態的必要信息,也易於調試。因此建議為每個自定義的類覆蓋 toString() 方法。

下面重寫 Student 類的 toString() 方法:

@Override
public String toString() {
    return getClass().getName() 
            + "{ sid=" + sid
            + ", name=" + super.getName()
            + ", age=" + super.getAge()
            + ", sex=" + super.getSex()
            + " }";
}

如果父類 Person 也重寫類 toString() 方法:

@Override
public String toString() {
    return getClass().getName() + 
            "{" + '\'' +
            "name='" + name + '\'' +
            ", age=" + age +
            ", sex='" + sex + '\'' +
            '}';
    }

並且子類的 toString() 也可以調用:

@Override
public String toString() {
    return super.toString() + "{sid = " + sid + "}";
}
// xxx.Student{'name='小咖', age=20, sex='男'}{sid = 1001}

clone()

Object 類提供了 clone() 方法用於克隆實例,但因為是 protected 修飾符所修飾的方法,因此不會顯示地覆蓋 clone() 。實現 Cloneable 接口的類可以覆蓋 clone() 方法提供克隆。如果不實現,會拋出 CloneNotSupportedException 異常。正確的寫法如下所示:

public class Person implements Cloneable {
    private String name;
    private int age;
    private String sex;
    private String[] address;
    public Person(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
    ...省略getter與setter...

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

Java 支持協變返回類型,也就是覆蓋方法的返回類型可以是被覆蓋方法的返回類型的子類。並且在 clone() 方法中調用 super.clone() 方法得到功能完整的克隆對象。

當使用 Person 創建對象並調用 clone() 方法克隆。

Person s1 = new Person("小卡", 22, "男");
s1.setAddress(new String[] {"浙江省", "江蘇省", "湖南省"});
Person s2 = sample.clone();
System.out.println(s1.hashCode()); // 873415566
System.out.println(s2.hashCode()); // 818403870
System.out.println(s1 == s2); // false
System.out.println(s1.getAddress() == s2.getAddress()); // true

從上面的代碼中得出,調用 clone() 方法獲得的對象是個新的對象,但是對象中的引用還是原來的引用,而不是新引用。這次的克隆被稱為 淺拷貝

使用 clone() 方法與通過構造器創建對象實際上是一樣的,要確保不會傷害到原始的對象,並確保正確地創建被克隆的對象中的約束條件。這次的克隆被稱為 深拷貝

因此,在 Person 類的內部,address 數組也要遞歸地調用 clone() 方法:

@Override
protected Person clone() throws CloneNotSupportedException {
    Person result = (Person) super.clone();
    result.address = address.clone();
    return result;
}

記住,Cloneable 與引用可變對象的 final 域的正常用法是不兼容的。因此,實現 clone() 方法禁止給 final 賦新值。

上述的拷貝方式比較複雜。可以在類中提供一個拷貝構造器或拷貝工廠來實現克隆的替代功能。

public Person(Person value) {...}
public static Person newInstance(Person value) {...}

finalize()

當 GC 確定不再有對該對象的引用時,GC 會調用對象的 finalize() 方法來清除回收。

protected void finalize() throws Throwable { }

因此,子類可以通過覆蓋此方法處理一些額外的清理工作。 但是,finalize() 方法何時被調用取決於 Java VM,而且不保證 finalize() 方法會被及時地執行 。因此,不要依賴 finalize() 方法來更新重要的持久狀態。

Java VM 會確保一個對象的 finalize() 方法只被調用一次,而且程序中不能直接調用 finalize() 方法。

finalize() 方法通常也不可預測,而且很危險,一般情況下,不必要覆蓋 finalize() 方法。

wait 與 notify

Object 對象提供了 wait()notify() 方法,這兩個方法的使用是相對的:

  • wait():線程進入等待狀態。
  • notify():喚醒等待該對象的線程。

使用 wait() 方法必須在同步區域內部調用,這個同步區域將對象鎖定在調用 wait() 方法的對象上。下面是使用 wait() 方法的標準模式:

synchronized (obj) {
    while (<condition does not hold>) {
        obj.wait();
    }
}

這時的 obj 對象所在線程會處於等待狀態,需要使用 notify() 方法喚醒 obj 所在線程。

synchronized (obj) {
    obj.notify();
}

最好使用 notifyAll() 方法喚醒線程。因為總會產生正確的結果,保證會喚醒所有需要被喚醒的線程。雖然也會喚醒其他線程,但不影響程序的正確性,而且 notifyAll() 方法代替 notify() 方法可以避免來自不相關線程的意外或惡意的等待。

注意:必須使用 synchronized,否則會報 IllegalMonitorStateException 異常。

總結

這裏對 Object 類做了一個簡單的瞭解,知道 Object類是一切類的基類,可以引用一切的引用類型,包括數組類型。且 Object 中提供的方法需要在適合的場景下覆蓋得到最佳的結果,最好始終都覆蓋 toString() 方法。

更多內容請關注公眾號「海人為記」

image

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.