談 Java Object 類別中 3 個讓你覆寫的方法
Tags: java
Published: 2022-10-06 15:05:07

大家都知道 Java 中整個繼承體系的最上方就是 [`Object`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Object.html) 類別, 它也是定義類別沒有指明繼承對象時預設的父類別, 因此 `Object` 提供了幾個讓你覆寫的方法, 以便能搭配 Java 的其他機制妥善運作, 本文就來看這幾個特別的方法。

## toString()

[`toString()`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Object.html#toString()) 是在你的類別建立的物件要轉換成字串時會被自動叫用的方法, 不過 `Object` 類別不可能未卜先知, 知道你的類別要轉換成什麼樣的文字內容, 因此預設的實作是轉成 **"類別名稱@物件的識別碼雜湊值"** 這樣的格式：

```java
jshell> class A {}
|  created class A

jshell> A a1 = new A()
a1 ==> A@5e8c92f4

jshell> A a2 = new A()
a2 ==> A@50134894

jshell> a1.toString()
$48 ==> "A@5e8c92f4"

jshell> a2.toString()
$49 ==> "A@50134894"
```

在 `@` 後面的 16 進位數字就是這個物件的**識別碼雜湊值**, 每一個物件都會擁有一個自己**專屬**的識別碼雜湊值, 要識別兩個物件是否為同一個物件, 可以看他們的識別碼雜湊值是否相同。你也可以透過  [`System.identityHashCode()`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/System.html#identityHashCode(java.lang.Object)) 取得物件的識別碼雜湊值：

```java
jshell> System.identityHashCode(a1)
$44 ==> 1586270964

jshell> Integer.toHexString(System.identityHashCode(a1))
$45 ==> "5e8c92f4"

jshell> Integer.toHexString(System.identityHashCode(a2))
$46 ==> "50134894"
```

你可以看到跟剛剛建立物件時看到的識別碼雜測值是一樣的。

你可以透過覆寫 `toString()` 方法轉換成適當的字串內容：

```java
jshell> class A {
   ...>     public String toString() {
   ...>         return "this is class A.";
   ...>     }
   ...> }
|  replaced class A
|    update replaced variable a1, reset to null

jshell> A a1 = new A()
a1 ==> this is class A.

jshell> a1.toString()
$10 ==> "this is class A."
  
jshell> a1
a1 ==> this is class A.

jshell> System.out.println(a1)
this is class A.
```

你可以看到不論是明確叫用 `toString()` 或是讓 jshell 自動轉換, 甚或是傳入 `System.out.println()`, 都會透過覆寫的方法轉換成指定內容的文字。對於自訂的類別, 請記得要覆寫 `toString()` 方法。

## equals()

[`equals()`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Objects.html#equals(java.lang.Object,java.lang.Object)) 是用來判斷兩個物件的內容是否相等, 這和 `==` 不一樣, `==` 是判斷兩個物件是否為同一個, 也可視為判斷兩個物件的識別碼雜湊值是否相同, 例如：

```java
jshell> class C {}
|  created class C

jshell> C c1 = new C()
c1 ==> C@51521cc1

jshell> C c2 = new C()
c2 ==> C@694f9431

jshell> C c3 = c1
c3 ==> C@51521cc1

jshell> c1 == c2
$35 ==> false

jshell> c1 == c3
$36 ==> true
```

你可以看到利用 `new` 建立的不同物件就會擁有不同的識別碼雜湊值。

不過 `Object` 並不知道怎麼比較我們自訂的類別所產生的物件, 因此預設的實作就跟 `==` 一樣, 如果你看 `String` 類別, 就會看到兩者的差別：

```java
jshell> String s1 = "hello"
s1 ==> "hello"

jshell> String s2 = "hello"
s2 ==> "hello"

jshell> String s3 = new String("hello")
s3 ==> "hello"

jshell> System.identityHashCode(s1)
$22 ==> 764308918

jshell> System.identityHashCode(s2)
$23 ==> 764308918

jshell> System.identityHashCode(s3)
$24 ==> 580024961
  
jshell> s1 == s2
$39 ==> true

jshell> s1.equals(s2)
$40 ==> true

jshell> s1 == s3
$41 ==> false

jshell> s1.equals(s3)
$42 ==> true
```

你可以看到只要字串內容相同, 不論是不是同一個物件, `equals()` 都會傳回 `true`, 但是 `==` 是比較他們是否為同一物件。

對於你自己的類別, 就應該要覆寫 `equals()`, 客製化比較內容是否相等的方法, 請看以下的例子：

```java
shell> class D { public int num;}
|  created class D

jshell> D d1 = new D()
d1 ==> D@6d5380c2

jshell> d1.num = 20
$3 ==> 20

jshell> D d2 = new D()
d2 ==> D@bebdb06

jshell> d2.num = 20
$5 ==> 20

jshell> d1 == d2
$6 ==> false

jshell> d1.equals(d2)
$7 ==> false
```

由於沒有覆寫 `equals()`, 會以預設的實作執行, 因此雖然兩個物件的內容相同, 但因為是不同的物件, 還是會被判訂為不相等。以下是覆寫 `equals()` 的版本：

```java
jshell> class D {
   ...>     public int num;
   ...>     public boolean equals(D obj) {
   ...>         return this.num == obj.num;
   ...>     }
   ...> }
|  replaced class D
|    update replaced variable d2, reset to null
|    update replaced variable d1, reset to null

jshell> D d1 = new D()
d1 ==> D@2d38eb89

jshell> d1.num = 20
$12 ==> 20

jshell> D d2 = new D()
d2 ==> D@4629104a

jshell> d2.num = 20
$14 ==> 20

jshell> d1 == d2
$15 ==> false

jshell> d1.equals(d2)
$16 ==> true
```

雖然 `d1` 和 `d2` 是不同的物件, 但因為內容相同, 就會判定為相等了。

## hashCode()

[`hashCode`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Object.html#hashCode()) 這個方法是為了搭配 Java 的 [`HashMap`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/HashMap.html) 這類需要以雜湊值 (hash value) 當索引鍵存取資料的結構使用。不過 `Object` 當然也無法預先知道你的類別建立的物件要如何計算雜湊值, 因此預設的實作就是傳回物件自己的識別碼雜湊值, 例如：

```java
jshell> class B {}
|  created class B

jshell> B b1 = new B()
b1 ==> B@573fd745

jshell> b1.hashCode()
$17 ==> 1463801669

jshell> Integer.toHexString(b1.hashCode())
$18 ==> "573fd745"
```

在自訂的類別中應該要覆寫這個方法, 由於這個方法的傳回值要當成索引鍵, 所以 Java 有明文規定 `hashCode()` 必須符合以下條件：

1. 同一物件不論叫用多少次 `hashCode()`, 都應該傳回相同的值。
2. 不同的物件如果叫用 `equals()` 得到 `true`, 那他們的 `toHash()` 就應該傳回相同的值。
3. 兩個物件如果叫用 `equals()` 得到 `false`, 他們的 `hashCode()` 並不一定要傳回不同的值。

以 `String` 類別來說, 只要字串內容相同, 不論是不是同一個物件, `hashCode()` 就會傳回相同的值。以前一小節的 `s1`~`s3` 這 3 個字串為例：

```java
jshell> s1.hashCode()
$25 ==> 99162322

jshell> s2.hashCode()
$26 ==> 99162322

jshell> s3.hashCode()
$27 ==> 99162322
```

由於 3 個字串內容相等, 所以取得的雜湊值都一樣。

`Object` 類別定義的 `hashCode()` 因為會傳回識別碼雜湊值, 會讓相同內容的不同物件產生不同的雜湊碼, 所以並不符合前述對於 `hashCode()` 的要求, 因此在定義類別時, 如果該類別的物件會用在需要雜湊值的情境下, 就應該要覆寫 `hashCode()`, 例如：

```java
jshell> class E {
   ...>     public int num;
   ...>     public int hashCode() {
   ...>         return Objects.hash(this.num);
   ...>     }
   ...> }
|  created class E

jshell> E e1 = new E()
e1 ==> E@1f

jshell> e1.num = 20
$26 ==> 20

jshell> E e2 = new E()
e2 ==> E@1f

jshell> e2.num = 20
$28 ==> 20

jshell> E e3 = new E()
e3 ==> E@1f

jshell> e3.num = 40
$30 ==> 40

jshell> e1.hashCode()
$31 ==> 51

jshell> e2.hashCode()
$32 ==> 51

jshell> e3.hashCode()
$33 ==> 71
```

我們直接採用 [`Objects`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Objects.html#hashCode(java.lang.Object)) 的 [`hash()`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Objects.html#hash(java.lang.Object...)) 類別方法幫我們計算雜湊值, 你可以看到現在 `e1` 和 `e2` 會產生相同的雜湊碼, 但 `e3` 因為其 `num` 值不同, 所以產生的雜湊碼就不相同。

## 小結

透過以上簡單的說明, 你應該已經瞭解在定義類別時, 依照需求覆寫特定的方法, 搭配運用情境正確運作了。