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



如果你建立了一個 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,你可以看看是不是你想要的。