【Guava 教學】(4)實作 toString、equals 與 hashCode 的幫手 by caterpillar | CodeData
top

【Guava 教學】(4)實作 toString、equals 與 hashCode 的幫手

分享:

【Guava 教學】(3)高階排序概念的實現 << 前情

如果你建立了一個 Point 類別:

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

並且用它來產生了一些 Point 實例並收集起來,然後在某個時候,打算顯示一下目前收集了哪個點:

List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2));
out.println(points);

執行結果只會顯示[guavademo.Point@139a55, guavademo.Point@1db9742]這種資訊,真的是沒什麼用,這就是為什麼你要定義 toString 方法。現代 IDE 有些會支援直接產生 toString,例如可以用 NetBeans 的 Insert Code 來產生如下的 toString

public class Point {
    ...
    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

IDE 幫你產生是不錯,不過要自己寫這些就不怎麼高興了,而且讀來也不怎麼好讀,改用 String.format 會好一些些:

public class Point {
    ...
    @Override
    public String toString() {
        return String.format("Point{x=%d, y=%d}", x, y);
    }
}

不過自己做字串格式化終究還是蠻麻煩的,你可以改用 Guava 的 Objects.toStringHelper 試試:

import com.google.common.base.Objects;
public class Point {
    ...
    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                      .add("x", x)
                      .add("y", y)
                      .toString();
    }
}

除了產生 toString 的幫手之外,Guava 在比較物件時也提供了 Objects.equal,這東西與 JDK7 的 Objects.equals 是相同作用的,如果你使用 JDK6 或之前的版本,則可以試試 Guava 的。怎麼用呢?因為 Objects.equal 蠻簡單的,單純解釋它沒意思,重點還是在於怎麼寫出正確的 equals 比較重要,那藉這個機會重新闡述一下 物件相等性(上) 這篇文章裏頭的一些東西好了,將裏頭第二個 Point 類別的定義改寫如下:

import com.google.common.base.Objects;

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

    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                      .add("x", x)
                      .add("y", y)
                      .toString();
    }

    @Override
    public boolean equals(Object that) {
        if(that instanceof Point) {
            Point p = (Point) that;
            return x.equals(p.x) && y.equals(p.y);
        }
        return false;
    }
}

不過,這個 equals 並不安全,如果 xynull 的話,那麼就會噴出 NullPointerException 了,自行加些 xy 是否為 null 的檢查是可以,不過我知道有 Objects.equal 可以協助,為什麼不拿來用?

...
public class Point {
    ...
    @Override
    public boolean equals(Object that) {
        if(that instanceof Point) {
            Point p = (Point) that;
            return Objects.equal(x, p.x) && Objects.equal(y, p.y);
        }
        return false;
    }
}

Object.equal 的原始碼很簡單,會幫你判斷參考與 null

...
  public static boolean equal(@Nullable Object a, @Nullable Object b) {
    return a == b || (a != null && a.equals(b));
  }
...

你可以使用目前的 Point 做相等性測試看看,像是 new Point(1, 1).equals(new Point(1, 1)),結果應該會是 true,那如果是以下的程式碼呢?

Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
Set<Point> pSet = new HashSet<>();
pSet.add(p1);
out.println(pSet.contains(p2));    // 可能顯示 false

如果上例結果顯示 false,並不用訝異,因為你在重新定義 equals 時,並沒有重新定義 hashCode。在許多場合,例如將物件加入群集 (Collection)時,會同時利用 equalshashCode 來判斷是否加入的是(實質上)相同的物件。在 Object 的 hashCode() 說明 指出:

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

HashSet 為例,會先使用 hashCode 得出該將物件放至哪個雜湊桶(Hash buckets)中,如果雜湊桶有物件,再進一步使用 equals 確定實質相等性,從而確定 Set 中不會有重複的物件。上例中說可能會顯示false ,是因為若湊巧物件 hashCode 算出在同一個雜湊桶,再進一步用 equals 就有可能出現 true

在重新定義 equals 時,最好重新一併重新定義 hashCode。只是 hashCode 該怎麼算呢?算出來的雜湊碼最好是儘量別重複,以免引起雜湊碰撞(Hash collision),過多的雜湊碰撞可能會有效能問題,甚至增加 hash collision dos 的可能性。

IDE 產生的 hashCode 通常比較簡單,例如 物件相等性(上) 中的 hashCode 實作,是舊版 NetBeans IDE 自動產生的程式碼:

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

如果使用 JDK7,那麼可以用 Objects.hash 來協助產生,如果是 JDK6 或先前版本,則可以使用 Guava 的 Objects.hashCode

...
    @Override
    public int hashCode() {
        return Objects.hashCode(x, y);
    }
...

實際上目前版本的 Guava 只是用了 JDK5 就有的 ArrayshashCode 方法而已:

...
  public static int hashCode(@Nullable Object... objects) {
    return Arrays.hashCode(objects);
  }
...

所以實際上,你應該看看 ArrayshashCode 上各個重載方法,瞭解它產生的 hashCode 是不是符合你的需求,就算你不使用 Guava,也不是在 JDK7 以上的版本,也知道可否使用 ArrayshashCode 為你產生適當的雜湊碼。

再次做剛剛的測試就會得到 true 了:

Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
Set<Point> pSet = new HashSet<>();
pSet.add(p1);
out.println(pSet.contains(p2));    // true

如果你沒瞭解過 equalshashCode 撰寫時,需要注意哪些事項,建議你繼續看看 物件相等性(上) 這篇文章,你也可以試著用 Guava 來簡化該篇文章的範例。

當然,你也可以讓 IDE 結合 Guava 來產生 equalshashCodetoString,如果你使用 IntelliJ IDEA,可以參考一下 IntelliJ IDEA: Generate equals, hashCode and toString with Google Guava

新版的 NetBeans 本身如果在 JDK7 平台上,產生的 equalshashCode 已經運用了 JDK7 的 Objects.equalsObjects.hashCode,你可以看看是不是你想要的。

後續 >> 【Guava 教學】(5)程式 90% 比率在管理與處理錯誤

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

Valentino Young07/07

觀察Guava API 至今, 不斷地看到對應apache commons有相同或相似的,
而在Guava之中的命名與呼叫設計方面更加賞心悅目...的各種工具,
每每引誘出 "下一個project開始就把 com.apache.common 換成 com.google.common 吧" 的念頭.

然而在 ToStringHelper(ToStringBuilder) 這方面, 卻看不到 ToStringStyle 的Guava版...

到Guava的forum上看了一下, 似乎是Guava的project member強烈反對所致.
(http://code.google.com/p/guava-libraries/issues/detail?id=400)
他們表示是基於 "簡潔, 結果一致" 原則設計Guava API,
並且認為95%以上的需求不會在乎Object#toString()所輸出的格式...

Object#toString() 本是給人類眼球方便的成分遠大於給電腦CPU方便的一種安排,
在多年的實務之中, 為了全文檢索與文字差異比較...等等目的, 陸續設計出幾個ToStringStyle,
並且由精心排版過的 Object#toString() 輸出,
才得以找出的BUG, 得以察覺的ISSUE, 得以產生的靈感...等等也不計其數.

不過回頭反省一下, 也是因為歷來承接下來的projects總是設計概念混亂, 程式碼混濁不明...等先天缺陷,
才會走利用 Object#toString() 來改善系統的旁門左道吧.

最近的重構工作碰到了 "若非幾萬個檔案一一打開起來用肉眼確認,
用各種檢索方式都無法區分那些legacy codes在用的究竟是物件本身?還是在用物件的toString()?" 的處境,
才體會到當 "Java基本語法/各類函式庫之中往往 someObject.toString() 可直接用 someObject 取代" 的安排,
與 "讓toString()直接產生業務邏輯所要的結果" 的習慣,
兩者長期交互作用之後, 會製造出多麼令人頭痛的問題...

回頭看guava forum上issue 400的建議與否決, 覺得 ToStringStyle 的問題在於他的出發點跟名稱.
將物件內容以pretty format呈現出來的需求仍會存在,
ToStringStyle之類的概念是很不錯的 --- 只要別用在 Object#toString() 上頭的話.

熱門論壇文章

熱門技術文章