2014年1月9日 星期四

檢查 Collection 中是否有某個自定義的 Object

一般如果使用 Collection 類別去儲存一系列的某種 Object 時,可以直接利用 Collection.contains(obj) 的方法檢查某個 Object 是否在 Collection 內
不過 Java 預設的 Object 的檢查方式是檢查兩個 Object 是否相等,是依據 Object 所在的記憶體位置判斷的
但有時我們想要的是 Object 內的某些屬性相等,就視這兩個 Object 相等
也就是當使用 Collection.contains(obj) 時,想要的結果其實是當 Collection 內的某個項目特定屬性跟 obj 的特定屬性一樣,就預期要回應 true。

根據官方文件中關於 Collection.contains() 的描述,可以看出它基本上是依據 Object.equals() 的結果來判斷指定的物件是否包含於 Collection 之中
因此想達到上述的自定義條件,只需要對 Collection 容納的 Object 類別去做自定義的 equals() 判斷即可。

舉例來說,以下是一個自定義的 Object,Object 的屬性是兩個字串。
public class TwoString {
  private String STR_1 = null;
  private String STR_2 = null;

  public TwoString (String str1, String str2) {
    this.STR_1 = str1;
    this.STR_2 = str2;
  }
}

接著我們想要在一個 Collection 當中放上述這個 TwoString 的類別,並且使用 contains() 的方法來比較,如下面的例子:
TwoString str1 = new TwoString("a", "b");
TwoString str2 = new TwoString("a", "b");
  
List<twostring> list = new LinkedList<twostring>();
list.add(str1);
  
System.out.println("Is str1 contained in the list? " + list.contains(str1));
System.out.println("Is str2 contained in the list? " + list.contains(str2));

實際執行時會獲得的是如下的結果:
Is str1 contained in the list? true
Is str2 contained in the list? false

這就是因為預設的比較方式是比較記憶體位置,因此即使兩個 Object 的屬性是完全一樣的,在 Collection.contains() 的測試結果仍然不會認為 str2 存在於 List 當中。

接著如果在 TwoString 上加上了覆寫既有的 equals() 的方法,如下:
public class TwoString {
  private String STR_1 = null;
  private String STR_2 = null;

  public TwoString (String str1, String str2) {
    this.STR_1 = str1;
    this.STR_2 = str2;
  }

  @Override
  public boolean equals (Object obj) {
    if(obj instanceof TwoString) {
      TwoString givenObj = (TwoString) obj;
      return (this.STR_1.compareTo(givenObj.STR_1) + this.STR_2.compareTo(givenObj.STR_2) == 0) ? true : false;
    }
    return false;
  }
}

再跑一次同樣的程式碼,獲得的結果就會變成這樣:
Is str1 contained in the list? true
Is str2 contained in the list? true

不過要特別注意的是,在某些狀況中 Collection 會同時使用 hashCode() 和 equals() 來判斷是否包含
因此當實作覆寫 equals() 時,最好一併要覆寫 hashCode(),以避免一些出乎意料的問題出現。
例如:
public class TwoString {
  private String STR_1 = null;
  private String STR_2 = null;

  public TwoString (String str1, String str2) {
    this.STR_1 = str1;
    this.STR_2 = str2;
  }

  @Override
  public boolean equals (Object obj) {
    if(obj instanceof TwoString) {
      TwoString givenObj = (TwoString) obj;
      return (this.STR_1.compareTo(givenObj.STR_1) + this.STR_2.compareTo(givenObj.STR_2) == 0) ? true : false;
    }
    return false;
  }

  @Override
  public int hashCode () {
    return this.STR_1.hashCode() + this.STR_2.hashCode();
  }
}

不過定義 equals() 和 hashCode() 時,官方有建議一些需要注意的事項,以下轉錄自 [3] 的內容。
在Object的 equals() 說明 中有提到,實作equals()時要遵守的約定:
  • 反身性(Reflexive):x.equals(x)的結果要是true。
  • 對稱性(Symmetric):x.equals(y)與y.equals(x)的結果必須相同。
  • 傳遞性(Transitive):x.equals(y)、y.equals(z)的結果都是true,則x.equals(z)的結果也必須是true。
  • 一致性(Consistent):同一個執行期間,對x.equals(y)的多次呼叫,結果必須相同。
  • 對任何非null的x,x.equals(null)必須傳回false。
關於如何正確地設計 equals() 和 hashCode(),建議可以看一下 [3]!

參考資料:
1、How does a Java Arraylist contains() method evaluate objects?
2、Javadoc:Collection.contains()
3、Java Essence: 物件相等性

沒有留言: