CodeGym /課程 /JAVA 25 SELF /equals, hashCode, toString: 自動產生

equals, hashCode, toString: 自動產生

JAVA 25 SELF
等級 22 , 課堂 2
開放

1. equals, hashCode, toString 的自動產生

為什麼需要這些方法?

在 Java 中與物件打交道時,你很快就會遇到相同的幾個需求。有時需要檢查兩個物件是否相等。例如,想知道某個物件是否已存在於像 SetMap 之類的集合中。在其他情況下,物件被用作 HashMap 的鍵,這時就離不開特定的比較規則。還有,幾乎總是會想把物件印在日誌或螢幕上,而且不想只是看到像 MyClass@7b23ec81 這樣的亂碼,而是一些有意義的內容。

針對這些情況,Java 中的每個類別都有三個特殊的方法:

  • equals(Object o) 負責檢查相等性。
  • hashCode() 產生物件的數值「指紋」,供雜湊表等集合使用。
  • toString() 回傳物件的易讀字串表示,對除錯與列印非常有幫助。

為什麼在一般類別裡會這麼痛苦?

在一般類別中,這些方法得手動撰寫。此時無聊與頭痛就開始了:產生一堆樣板程式碼,只會讓類別變得臃腫,而且非常容易在某處出錯——忘了比較某個欄位、錯算了 hashCode,然後就得面對各種詭異的 bug。若之後在類別中新增欄位——還得再回去修改這些方法,全部重來。

一般類別的範例


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

看起來很眼熟吧?是的,這還只是兩個欄位!如果有二十個呢?

record 怎麼做

Record 類會幫你完成這一切。只要宣告:


public record Point(int x, int y) { }

Java 會自動產生:

  • 建構子
  • getter(x()y()
  • equalshashCodetoString

自動產生的方法

  • equals 會逐一比較 record 的所有元件值。
  • hashCode 會依所有元件計算。
  • toString 回傳格式為 Point[x=1, y=2] 的字串。

來實際看一下吧!


public record Point(int x, int y) {}

public class Demo {
    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true
        System.out.println(p1); // Point[x=1, y=2]
    }
}

輸出:

true
true
Point[x=1, y=2]

一切如預期運作,且不必多寫任何一行程式碼!

2. 為什麼這很重要:集合、除錯與健壯性

在集合中的正確運作

想像你把物件當成 HashMap 的鍵或 HashSet 的元素。如果 equalshashCode 的實作不正確——集合就會表現得很怪:找不到你剛加進去的元素,或者反過來,把兩個不同的物件當成一樣。

使用 record 類時你可以放心:比較與雜湊總會涵蓋所有 record 元件(按照宣告時的順序)。

範例:把 record 當成鍵


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record Point(int x, int y) {}

        Map<Point, String> map = new HashMap<>();
        Point p1 = new Point(3, 4);
        map.put(p1, "Hello!");

        Point p2 = new Point(3, 4);
        System.out.println(map.get(p2)); // "Hello!" — 可以運作!
    }
}

請注意:p1p2 是不同物件(不同參考),但欄位值相同,因此被視為相等。關於 MapHashMap 的更多內容,你會在第 26 級學到 :P

方便的除錯與記錄

與其看到乏味的 Point@1a2b3c4d(一般類別的預設),record 類會輸出漂亮又具資訊性的格式:

Point[x=3, y=4]

這能大幅節省除錯與記錄時的時間。

3. record 內部的 equals, hashCode, toString 是怎麼運作的

equals 方法

Record 類的 equals 讓兩個物件在以下情況下被視為相等:

  • 它們是相同型別(同一個 record 類)
  • 所有元件都相等(對於基本型別用 ==,對於物件用 equals()

比較範例


Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);

System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false

hashCode 方法

雜湊碼會根據 record 的所有元件計算,通常使用標準方法 Objects.hash(...)


System.out.println(p1.hashCode()); // 例如,994
System.out.println(p2.hashCode()); // 也是 994
System.out.println(p3.hashCode()); // 不同的數字

toString 方法

字串表示永遠是這個格式:

ClassName[field1=value1, field2=value2, ...]

System.out.println(p1); // Point[x=1, y=2]

4. 覆寫 equals, hashCode, toString:何時、如何做?

有時候(很少,但確實會)需要改變這些方法的預設行為。比方說,你想讓 toString 回傳不同的格式,或只用部分欄位來比較。

注意:如果你要覆寫 equals/hashCode,務必非常謹慎!破壞它們的「契約」可能導致很難抓的 bug。

如何覆寫方法

只要在 record 類的主體內宣告你自己的方法即可:


public record Point(int x, int y) {
    @Override
    public String toString() {
        return "(" + x + "; " + y + ")";
    }
}

Point p = new Point(3, 5);
System.out.println(p); // (3; 5)

可以覆寫 equals/hashCode 嗎?

可以,但如果不確定自己在做什麼,強烈不建議。比方說,你想只用欄位 x 來比較(這其實很奇怪):


public record Point(int x, int y) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(x);
    }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);

System.out.println(p1.equals(p2)); // true (!)

但請務必小心:如果你覆寫了 equals,也一定要覆寫 hashCode——否則集合就會工作不正常。

Best practice

  • 如果不確定為什麼要覆寫——就不要覆寫!
  • 對於 toString——如果想要自訂格式,可以放心去做。
  • 對於 equals/hashCode——只有在有充分理由且理解其後果時才這麼做。

5. 實作:物件比較與在集合中使用 record

範例:比較兩個 record 物件


public record User(String name, int age) {}

public class Demo {
    public static void main(String[] args) {
        User u1 = new User("Alice", 20);
        User u2 = new User("Alice", 20);
        User u3 = new User("Bob", 25);

        System.out.println(u1.equals(u2)); // true
        System.out.println(u1.equals(u3)); // false

        System.out.println(u1.hashCode() == u2.hashCode()); // true
        System.out.println(u1); // User[name=Alice, age=20]
    }
}

範例:將 record 當作 HashMap 的鍵

讓我們假設有個應用程式,會依使用者的名字與年齡來儲存造訪次數(說不定俱樂部裡有兩個「Ivan 20 歲」)。


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record User(String name, int age) {}

        Map<User, Integer> visits = new HashMap<>();
        User ivan20 = new User("Ivan", 20);
        User ivan22 = new User("Ivan", 22);

        visits.put(ivan20, 5);
        visits.put(ivan22, 2);

        // 來確認依值查找是否能正確運作
        System.out.println(visits.get(new User("Ivan", 20))); // 5
        System.out.println(visits.get(new User("Ivan", 22))); // 2
    }
}

如果 equalshashCode 沒有正確實作,查找就不會成功。更多關於 MapHashMap 的內容,請參考第 26 級的課程 :P

6. 在 record 類中使用 equals, hashCode, toString 的常見錯誤

錯誤 №1:以為建立之後還能修改欄位。
Record 的欄位永遠是 final,比較會依其在建構子中給定的值進行。如果你用某種取巧方式「改變」內部狀態(例如欄位裡放可變物件並修改其內容),比較與雜湊就可能變得不正確。

錯誤 №2:覆寫了 equals,卻忘了 hashCode。
如果你覆寫了其中一個方法——一定也要覆寫另一個!否則集合(HashSetHashMap)會表現得不可預期。

錯誤 №3:期待 toString 會有其他格式。
如果你需要特定格式——直接覆寫 toString。預設格式永遠是 ClassName[field1=value1, field2=value2]

錯誤 №4:將 record 用在含有可變欄位的複雜類別。
Record 的欄位應該是不可變的。如果作為欄位的是 ArrayList 之類的型別,而且有人修改了其內容——比較與雜湊碼就可能「壞掉」。Record 最好只搭配不可變型別。

錯誤 №5:把 record 用在不屬於 value-object 行為的類別上。
Record 並不是「語法更短的小類別」。它就是 value object,專門用來保存一組值。如果你的類別有複雜邏輯、可變狀態或需要繼承——請使用一般類別。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION