深入研究Java equals方法 – JAVA編程語言程序開發技術文章

Java equals方法的重要性毋須多言,隻要你想比較兩個對象是不是同一對象,你就應該實現equals方法,讓對象用你認為相等的條件來進行比較.


  下面的內容隻是API的規范,沒有什麼太高深的意義,但我之所以最先把它列在這兒,是因為這些規范在事實中並不是真正能保證得到實現.


  1.對於任何引用類型, o.equals(o) == true成立.


  2.如果 o.equals(o1) == true 成立,那麼o1.equals(o)==true也一定要成立.


  3.如果 o.equals(o1) == true 成立且 o.equals(o2) == true 成立,那麼


  o1.equals(o2) == true 也成立.


  4.如果第一次調用o.equals(o1) == true成立,在o和o1沒有改變的情況下以後的任何次調用都成立.


  5.o.equals(null) == true 任何時間都不成立.


  以上幾條規則並不是最完整的表述,詳細的請參見API文檔.對於Object類,它提供瞭一個最最嚴密的實現,那就是隻有是同一對象時,equals方法才返回true,也就是人們常說的引用比較而不是值比較.這個實現嚴密得已經沒有什麼實際的意義, 所以在具體子類(相對於Object來說)中,如果我們要進行對象的值比較,就必須實現自己的equals方法.先來看一下以下這段程序:


以下是引用片段:
  public boolean equals(Object obj)
  {
  if (obj == null) return false;
  if (!(obj instanceof FieldPosition))
  return false;
  FieldPosition other = (FieldPosition) obj;
  if (attribute == null) {
  if (other.attribute != null) {
  return false;
  }
  }
  else if (!attribute.equals(other.attribute)) {
  return false;
  }
  return (beginIndex == other.beginIndex
  && endIndex == other.endIndex
  && field == other.field);
  }



  這是JDK中java.text.FieldPosition的標準實現,似乎沒有什麼可說的. 我信相大多數或絕大多數程序員認為,這是正確的合法的equals實現.畢竟它是JDK的API實現啊. 還是讓我們以事實來說話吧:


以下是引用片段:
  package debug
  ;import java.text.*;
  public class Test {
  public static void main(String[] args) {
  FieldPosition fp = new FieldPosition(10);
  FieldPosition fp1 = new MyTest(10);
  System.out.println(fp.equals(fp1));
  System.out.println(fp1.equals(fp));
  }
  }
  class MyTest extends FieldPosition{
  int x = 10;
  public MyTest(int x){
  super(x);
  this.x = x;
  }
  public boolean equals(Object o){
  if(o==null) return false;
  if(!(o instanceof MyTest )) return false;
  return ((MyTest)o).x == this.x;
  }
  }



  運行一下看看會打印出什麼:


以下是引用片段:
  System.out.println(fp.equals(fp1));打印true
  System.out.println(fp1.equals(fp));打印flase



  兩個對象,出現瞭不對稱的equals算法.問題出在哪裡(腦筋急轉彎:當然出在JDK實現的BUG)?我相信有太多的程序員(除瞭那些根本不知道實現equals方法的程序員外)在實現equals方法時都用過instanceof運行符來進行短路優化的,實事求是地說很長一段時間我也這麼用過。


  太多的教程,文檔都給瞭我們這樣的誤導。而有些稍有瞭解的程序員可能知道這樣的優化可能有些不對但找不出問題的關鍵。另外一種極端是知道這個技術缺陷的骨灰級專傢就提議不要這樣應用。我們知道,”通常”要對兩個對象進行比較,那麼它們”應該”是同一類型。所以首先利用instanceof運算符進行短路優化,如果被比較的對象不和當前對象是同一類型則不用比較返回false。


  但事實上,”子類是父類的一個實例”,所以如果子類 o instanceof 父類,始終返回true,這時肯定不會發生短路優化,下面的比較有可能出現多種情況,一種是不能造型成父類而拋出異常,另一種是父類的private 成員沒有被子類繼承而不能進行比較,還有就是形成上面這種不對稱比較。可能會出現太多的情況。


  那麼,是不是就不能用 instanceof運算符來進行優化?答案是否定的,JDK中仍然有很多實現是正確的,如果一個class是final的,明知它不可能有子類,為什麼不用 instanceof來優化呢?為瞭維護SUN的開發小組的聲譽,我不說明哪個類中,但有一個小組成員在用這個方法優化時在後加加上瞭加上瞭這樣的註釋:


以下是引用片段:
  if (this == obj) // quick check
  return true;
  if (!(obj instanceof XXXXClass)) // (1) same object?
  return false;



  可能是有些疑問,但不知道如何做(不知道為什麼沒有打電話給我……)那麼對於非final類,如何進行類型的quick check呢?


以下是引用片段:
  if(obj.getClass() != XXXClass.class) return false;



  用被比較對象的class對象和當前對象的class比較,看起來是沒有問題,但是,如果這個類的子類沒有重新實現equals方法,那麼子類在比較的時候,obj.getClass() 肯定不等於XXXCalss.class, 也就是子類的equals將無效,所以


以下是引用片段:
  if(obj.getClass() != this.getClass()) return false;



  才是正確的比較。另外一個quick check是if(this==obj) return true;


 是否equals方法比較的兩個對象一定是要同一類型?上面我用瞭”通常”,這也是絕大多數程序員的願望,但是有些特殊的情況,我們可以進行不同類型的比較,這並不違反規范。但這種特殊情況是非常罕見的,一個不恰當的例子是,Integer類的equals可以和Sort做比較,比較它們的value是不是同一數學值。(事實上JDK的API中並沒有這樣做,所以我才說是不恰當的例子)在完成quick check以後,我們就要真正實現你認為的“相等”。對於如果實現對象相等,沒有太高的要求,比如你自己實現的“人”類,你可以認為隻要name相同即認為它們是相等的,其它的sex, ago都可以不考慮。這是不完全實現,但是如果是完全實現,即要求所有的屬性都是相同的,那麼如何實現equals方法?


以下是引用片段:
  class Human{
  private String name;
  private int ago;
  private String sex;
  ………………..
  public boolean equals(Object obj){
  quick check…….
  Human other = (Human)ojb;
  return this.name.equals(other.name) && this.ago == ohter.ago && this.sex.equals(other.sex);
  }
  }



  這是一個完全實現,但是,有時equals實現是在父類中實現,而要求被子類繼承後equals能正確的工作,這時你並不事實知道子類到底擴展瞭哪些屬性,所以用上面的方法無法使equals得到完全實現。


  一個好的方法是利用反射來對equals進行完全實現:


以下是引用片段:
  public boolean equals(Object obj){
  quick check…….
  Class c = this.getClass();
  Filed[] fds = c.getDeclaredFields();
  for(Filed f:fds){
  if(!f.get(this).equals(f.get(obj)))
  return false;
  }
  return true;
  }



  為瞭說明的方便,上明的實現省略瞭異常,這樣的實現放在父類中,可以保證你的子類的equals可以按你的願望正確地工作。關於equals方法的最後一點是:如果你要是自己重寫(正確說應該是履蓋)瞭equals方法,那同時就一定要重寫hashCode().這是規范,否則………….


  我們還是看一下這個例子:


以下是引用片段:
  public final class PhoneNumber {
  private final int areaCode;
  private final int exchange;
  private final int extension;
  public PhoneNumber(int areaCode, int exchange, int extension) {
  rangeCheck(areaCode, 999, “area code”);
  rangeCheck(exchange, 99999999, “exchange”);
  rangeCheck(extension, 9999, “extension”);
  this.areaCode = areaCode;
  this.exchange = exchange;
  this.extension = extension;
  }
  private static void rangeCheck(int arg, int max, String name) {
  if(arg < 0 || arg > max)
  throw new IllegalArgumentException(name + “: ” + arg);
  }
  public boolean equals(Object o) {
  if(o == this)
  return true;
  if(!(o instanceof PhoneNumber))
  return false;
  PhoneNumber pn = (PhoneNumber)o;
  return pn.extension == extension && pn.exchange == exchange && pn.areaCode == areaCode;
  }
  }



  註意這個類是final的,所以這個equals實現沒有什麼問題。我們來測試一下:


以下是引用片段:
  public static void main(String[] args) {
  Map hm = new HashMap();
  PhoneNumber pn = new PhoneNumber(123, 38942, 230);
  hm.put(pn, “I love you”);
  PhoneNumber pn1 = new PhoneNumber(123, 38942, 230);
  System.out.println(pn);
  System.out.println(“pn.equals(pn1) is ” + pn.equals(pn1));
  System.out.println(hm.get(pn1));
  System.out.println(hm.get(pn));

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *