11-5 認識 Object 類別

在 Java 中,所有的物件都隱含的擴充了 java.lang.Object 類別,Object 類別是 Java 程式中所有類別的父類別,當您定義一個類別時:

public class Foo {
    // 實作
}
                            

這個程式碼相當於:

public class Foo extends Object {
    // 實作
}
                            

圖11-16 Object 是 Java 中所有類別之父類別

Object 中定義了許多個方法,包括公開的(public) equals()、toString() 、 hashCode() 、 getClass() 、 wait() 、 notify() 、notifyAll(),保護的(protected)clone()、finalize(),這些方法中 getClass()、notify()、notifyAll()、wait()被宣告為"final",所以您無法重新定義這些方法,而其它的方法您都可以重新定義。

clone()、getClass()、notify()、notifyAll()、wait()的使用是進階課題,不在本單元中介紹,以下則將介紹 toString()、finalize()、equals()、hashCode()的使用。

• toString()方法

Object 的 toString()方法目的是傳回物件本身的描述,預設上Object 的 toString()方法會傳回以下的內容:

public String toString() {
    return getClass().getName() + '@' + Integer.toHexString(hashCode());
}
                            

getClass()方法會取得類別於 JVM 中的 Class 實例,呼叫該實例的 getName()將傳回類別名稱;hashCode()則是傳回實例的「雜湊碼」(Hash code)。

「雜湊碼」(Hash code)由雜湊函式計算得到,在資料結構上可用於資料的定址,在 Java 中即使您不是很了解其原理與作用,仍可以直接使用相關類別來得到相同的好處,您可以參考資料結構專書了解雜湊碼的原理與作用。

在 Java SE 中,每個類別都會適當的重新定義 toString()方法,以傳回相關的物件描述,直接來看幾個例子,程式碼 11-15 將直接呼叫各個物件的 toString()方法來顯示物件的描述。

public class ToStringDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        int[] array1 = {1, 2, 3, 4, 5};
        double[] array2 = {1.0, 2.0, 3.0};
        String str = "Java";
        System.out.println(obj.toString());
        System.out.println(array1.toString());
        System.out.println(array2.toString());
        System.out.println(str.toString());
    }
}
                            
程式碼 11-15 ToStringDemo.java

圖11-17 程式碼 11-15 的執行結果

在 Java 中,陣列是以物件的方式存在,而且在 JVM 中會有一個代表它的類別實例存在,名稱為[開始,例如[I 表示 int[] 型態陣列物件,[D 表示 double[]陣列物件,因而程式碼 11-15 顯示的會是像[I@757aef 與[D@d9f9c3 的字樣,而 String 類別的 toString()方法,傳回的是本身的字串內容。

事實上 System.out 的 println()等方法如果直接給它一個物件作為引數,則會自動呼叫該物件的 toString()方法,所以程式碼中的第 8 行到第 11 行,也可以直接如下撰寫:

System.out.println(obj);
System.out.println(array1);
System.out.println(array2);
System.out.println(str);
                            

您也可以在繼承了某類別時,重新定義 toString()方法,為該類別加上適當的字串描述,例如在程式碼 11-13 中重新定義toString()方法:

public class Pants extends Clothes {
    private int waistline;
    ...
    public String toString() {
        return size + "," + waistline + "," + price;
    }
}
                            

• finalize()方法

在 Java 中建立的物件,如果沒有被任何的名稱參考,它將會被 JVM 回收,以釋放物件所佔據的資源,例如:

圖11-18 沒有名稱參考的物件收會被回收

在圖 11-7 中,object1、object2、object4 三個物件有被名稱參考,而 object3 沒有被任何名稱參考,也就是不會有方式可以使用到 object3,JVM 發現到這樣一個物件時,會在適當的時候回收該物件。

在 JVM 回收物件之前,會執行物件的 finalize()方法,您可以在這個方法中撰寫一些物件被回收前的善後工作,然而要注意的是,JVM 何時會回收物件並無法預知,所以如果您有立即性必須馬上處理的工作,不可以依賴在 finalize()方法中完成,當您讓某個物件不再被參考,JVM 回收它的時間是在一分鐘、五分鐘或十分鐘之後回收物件並不可得知,所以您只可以在 finalize()中撰寫一些非即時必須處理的善後工作,例如留下操作簡單的記錄(log)之類,而不能放一些像是釋放資料庫連結的動作。

在物件導向程式中,物件往往在程式的各個角落被參考,何時該回收資源是件複雜且難以判斷的工作,自動回收資源是Java 的垃圾收集( Garbage collection )機制,為的是讓開發人員不用費心於物件何時該釋放資源,必要的時候您可以建議 JVM 進行物件的回收,但也只能建議,因為 JVM 並不一定採納您的建議,當程式中有更高優先權的執行緒(Thread)在執行時,您的建議會被忽略。

程式碼 11-16 示範了使用 finalize()方法,並在 main 中讓建立的物件不再被參考,以測試 finalize()方法是否被執行:

public class Some {
    private String name;
    public Some(String name) {
        this.name = name;
        System.out.println(name + ": 被建立");
    }
    protected void finalize() {
        System.out.println(name + ": 被回收");
    }

    public static void main(String[] args) {
        Some some1 = new Some("Object 1");
        Some some3 = new Some("Object 3");
        Some some2 = new Some("Object 2");

        some1 = null;
        some2 = null;
        some3 = null;

        // 建議回收資源
        System.gc();
        System.out.println("程式結束");
    }
}
                            
程式碼 11-16 Some.java

程式中建立了三個 Some 類別的實例,並分別給予不同的名稱,之後在第 17 行至第 19 行讓 some1、some2、some3 分別參考至 null,而原來它們所參考的物件就不再被任何名稱參考,此時建議 JVM 回收物件,如果 JVM 採納建議,就會有以下的執行結果:

圖11-19 程式碼 11-16 的執行結果

圖11-20 物件回收前會執行 finalize 方法

• equals()、hashCode()方法

Object 的 equals()方法預設是比較物件的記憶體參考是否相同,開啟 Object 類別的原始碼,您也發現它的 equals()只是使用==運算子來比較:

public boolean equals(Object obj) {
    return (this == obj);
}
                            

在定義類別之時,通常建議重新定義 equals()方法,以定義您的物件之相等性,例如 String 類別就重新定義了 equals() 方法,在比較兩個 String 實例時,如果 String 物件的內含字元值都相同,視為兩個物件相等。

如果您要定義物件的相等性,在定義 equals()方法時,通常建議一併重新定義 hashCode()方法,程式碼 11-17 示範了如何定義一個 Student,並重新定義 equals()與 hashCode():

public class Student {
    private String name;
    private int number;

    public Student() {
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setNumber(int number) {
        this.number = number;
    }
    public int getNumber() {
        return number;
    }
    // 重新定義 equals()
    public boolean equals(Object other) {
        if (this == other)
            return true;
        if (!(other instanceof Student))
            return false;

        final Student student = (Student) other;
        if (!getName().equals(student.getName()))
            return false;
        if (number != student.getNumber())
            return false;

        return true;
    }
    // 重新定義 hashCode()
    public int hashCode() {
        int result = getName().hashCode();
        result = result + number;
        return result;
    }
}
                            
程式碼 11-17 Student.java

程式中的第 21 行首先使用==運算子測試兩個物件是否為同一物件,如果是的話直接返回 true 而不用再測試是否相等,接下來第 23 行使用 instanceof 運算子測試兩個物件是否為同一個類別的實例,如果不是就直接返回 false 而無須再往下進行比較,接著第 26 行到第 30 行都是在比較物件的資料成員值是否相同,這是很常見的比較物件相等性的方法,如果資料成員值有一個不同就返回 false,在所在的測試都通過的話,第 32 行返回 true。

在 hashCode()方法的設計上,這邊僅簡單的取得 name 屬性的雜碼碼,並加上 number 屬性的值,這只是個簡單的示範,hashCode實際上在設計時得依需求來決定如何製作 hash 碼,可以參考 API 文件中 Object 類別對 hashCode()之建議:

  • 在同一個應用程式執行期間,對同一物件呼叫hashCode()方法,必須回傳相同的整數結果。
  • 如果兩個物件使用 equals(Object)測試結果為相等, 則這兩個物件呼叫 hashCode()時,必須獲得相同的整數結果。
  • 如果兩個物件使用 equals(Object)測試結果為不相等,則這兩個物件呼叫 hashCode()時,要獲得不同的整數結果。