聽說你還不會實現equals方法?收藏這篇文章就夠了!

1、何時需要重寫equals

相信javaer們應該都知道equals方法,它是基類大佬Object中的一個方法,所以java下面所有的類都“自帶”這個方法。看方法名就知道,意圖就是對比傳入的目標對象, 跟自己是否“相等”。我們先看看這個方法在Object類中的實現:

    public boolean equals(Object obj) {
        return (this == obj);
    }

這個實現也算簡單粗暴了,直接用“==”來跟目標對象作比較,這個意圖就是:除非對方就是是自己本身,否則就不相等。 但就是因爲這樣的粗暴,造成限定得太死了。實際開發中,可能原生的equals不能滿足業務的需求。所以需要重寫,例如Integer中重寫的equals方法,可以看到Integer並不強制目標對象是“自己本體”,而只是對比了被自己包裝的int基本類型數值,這樣的實現更加符合實際業務要求:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();//對比被包裝的整型數值
        }
        return false;
    }

關於何時才需要重寫equals方法,《Effective Java》中提到:

如果類具有自己特有的“邏輯相等”概念,而且超類還沒有覆蓋equals。這通常屬於“值類”的情形。值類僅僅是一個表示值得類,例如Integer和String。程序員在利用equals方法來比較值對象的引用時,希望知道他們在邏輯上是否相等,而不是想了解它們是否指向同一個對象。

這句話翻譯得有點拗口,不過意思還是明確的,何時纔是需要重寫equals,可以總結爲以下兩點:

  • **該類只代表一個具體的“值“。**例如Double、Integer、Long等這種包裝類型,其實這些類再怎麼複雜,它們也只是代表一個數值,只要類型和數值相等,它們對象也理應是相等的。
  • **業務邏輯相同的對象。**這個可以看作是第一點的延申,即對象間的成員變量、行爲表現是一致的時候,也應該理解成是相同的對象。例如一個商品類Goods,只要它的商品ID,商品名稱等關鍵信息是相等的,就是同一件商品。

2、重寫equals方法要遵守的約定

2.1 重寫equals錯誤示範

重寫equals方法看似很簡單,但是有很多重寫的方式會導致意想不到的錯誤。舉一個jdk裏的一個錯誤例子,java.sql.Timestamp是繼承自java.util.Date的,這兩個類都分別重寫了equals方法。我們可以來看看:

java.util.Date.equals:

  public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
  }

java.sql.Timestamp:

    public boolean equals(java.lang.Object ts) {
      if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
      } else {
        return false;
      }
    }

這兩個類的equals方法,如果是各自單獨使用起來的話,是沒有任何問題的,但如果Date和Timestamp這兩個類一起使用的話就有問題了,例如把這兩個類的對象都存到一個集合list裏,就會出問題了:

	static List<java.util.Date> dateList = new ArrayList<>();
	public static void addTimeObj(java.util.Date date) {
		if(!dateList.contains(date)) {
			dateList.add(date);
		}
	}
	
	public static void main(String[] args) {
		long currentTimeMillis = 1586669016742L;
		java.sql.Timestamp timestamp = new Timestamp(currentTimeMillis);
		Date date = new java.util.Date(currentTimeMillis);
		
		System.out.println(date.equals(timestamp));//true
		System.out.println(timestamp.equals(date));//false
		
		addTimeObj(timestamp);
		addTimeObj(date);//date對象加不進去
		
	}

由上面的代碼可以看出,date.equals(timestamp)是true,timestamp.equals(date)是false,這個違反了下面將要說的對稱性 ,因此有可能會產生意想不到的後果,例如第二個addTimeObj方法的調用,date是加不進去的,因爲ArrayList.contains利用了對象的equals方法來進行對比的,然而timestamp.equals(date)==false。

     public boolean contains(Object o){
        //...省略一些代碼
        for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))//timestamp.equals(date)==false
                    return i;           
       return false;
        //...省略一些代碼
     }

2.2 重寫equals要遵守的”軍規“

所以,重寫equals時,是有一定的約束的,《Effective Java》中提到了一些重寫必須遵循的”軍規“:

  • 自反性:對於任何非null的引用值x,x.equals(x)必須返回true;
  • 對稱性:對於任何非null的引用x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true;
  • 傳遞性:對於任何非null的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)返回true,則x.equals(z)返回true;
  • 一致性:對於任何非null的引用值x和y,只要equals的比較操作對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者一致地返回false;
  • 對於任何非null的引用x,x.equals(null)必須返回false。

以上幾條規定,如果違反了其中一條,就有可能會產生意想不到的後果,程序也許會表現得不正常,甚至崩潰。就像上面java.util.Date和java.sql.Timestamp的例子,這樣的bug是很難排查出來的。

3、如何正確快速的重寫equals方法

我們得知重寫equals方法的一些約束後,會覺得,實現一個equals方法怎麼那麼難?其實,要實現equals方法,並不難,在實際開發中,有以下兩個快速穩當的方法。

3.1 利用IDE的自動生成equals

目前的IDE都有equals方法的快捷生成方法,比如eclipse,”Alt+Shift+S“組合鍵—>Generate hashCode() and equals(),即可同時生成hashCode方法和equals方法*(由此也可以看出equals方法和hashCode方法是捆綁一起的,實現equals方法,必須也得實現hashCode方法)*

public class Graph {

	int n;
	
	LinkedList<Integer> [] table;

   //....此處省略了hashCode方法的實現

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Graph other = (Graph) obj;
		if (n != other.n)
			return false;
		if (!Arrays.equals(table, other.table))
			return false;
		return true;
	}
}

上面的equals方法是eclipse生成的(篇幅原因省略了hashCode的代碼),粗略的看起來還挺全的,其實不一定符合我們實際開發中的業務需求,而且略顯臃腫。所以在平時開發中用IDE生成的這種代碼,往往都是需要調整一下,以適應實際業務,當然,前提是要遵循上面說到的幾個軍規

3.2 重寫equals方法的幾個訣竅(步驟)

IDE生成equals方法非常便捷,但是生成的代碼有時是比較”雞肋“。因此,我們是否可以在遵循軍規的基礎上,自己快速手寫一個equals方法呢?當然是可以的,可以按照以下幾個步驟來一步步實現。

  1. 使用**==**操作符檢查參數是否爲這個對象的引用。如果是,則返回true。
  2. 使用instanceof操作符檢查參數是否是正確的類型。如果不是,返回false。
  3. 把參數轉成正確的類型。因爲已經經過第二步的ininstanceof 校驗,所以類型會轉換成功。
  4. 對於該類中每個”關鍵域“,檢查參數中的域是否與該對象中的對應的域相匹配。在檢查域的時候,應該先檢查性能開銷最低的域,或者邏輯最可能不一致的域,按照這樣的有序安排可以儘可能減少比較的次數,從而提高性能。
  5. 編寫玩equals之後,應該考慮並檢驗三個問題:它是否是對稱的、傳遞的、一致的?

根據這五個步驟,寫出來的equals纔是安全可靠的,下面構造了一個Goods類,並實現了它的equals方法和hashCode方法,最後再來檢測下它的對稱性、傳遞性、一致性,都是可以的!

public class Goods {

	int id;	
	String goodsName;
	
	@Override
	public boolean equals(Object obj) {
		//使用==操作符檢查參數是否爲這個對象的引用。如果是,則返回true。
		if(this == obj) 
			return true;
		
		//使用instanceof操作符檢查參數是否是正確的類型。如果不是,返回false。
		if(!(obj instanceof Goods)) 
			return false;
		
		//把參數轉成正確的類型。
		Goods target = (Goods)obj;
		
		/*
		 * 對於該類中每個”關鍵域“,檢查參數中的域是否與該對象中的對應的域相匹配。
		 * 在檢查域的時候,應該先檢查性能開銷最低的域,或者邏輯最可能不一致的域,
		 * 按照這樣的有序安排可以儘可能減少比較的次數,從而提高性能。
		 */
		if(this.id != target.id)
			return false;
		if(target.goodsName == null || !target.goodsName.equals(this.goodsName))
			return false;
		
		return true;
	}
    
    @Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((goodsName == null) ? 0 : goodsName.hashCode());
		result = prime * result + id;
		return result;
	}
    
    
}

4、寫在最後

覆蓋equals方法是要比較嚴謹的,如果不是迫不得已,就不要輕易去重寫equals方法。如果要重寫,則一定要比較這個類的所有關鍵域,並且查看它們是否遵循equals的幾條軍規!而且還有另外一個很重要的是,重寫equals方法後,必須同步實現hashCode方法,否則就會違反了hashCode的通用約定,而關於hashCode 如何重寫,請看姐妹篇《如何實現高效的hashCode方法》

equals方法,你今天掌握了嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章