本文探討的是老掉牙的基礎問題,先建個實體類
package com.demo.tools; public class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
測試:
public static void main(String[] args){ User a = new User("A"); User b = new User("A"); System.out.println(a.equals(b)); }
輸出 false。明明存的都是A,爲啥equal的結果是false呢?不是說【equal比較的是值,==比較的是地址】嗎?因爲默認創建的類是繼承Object的,看一下Object類裏面的兩個方法:
/** * 返回對象的哈希碼值。 * * {@code hashCode}的一般約定爲: * 在Java應用程序執行期間對同一對象多次調用{@code hashcode}方法時,只要在對象{@code equals}的比較中使用的信息沒有被修改,則該方法必須一致地返回相同的整數。 * 從應用程序的一次執行到同一應用程序的另一次執行,此整數不必保持一致。 * * 如果調用{@code equals(object)}方法,兩個對象相等,那麼兩個對象調用{@code hashcode}方法必須產生相同的整數結果。 * 根據{@link java.lang.object equals(java.lang.object)}方法,如果兩個對象不相等,則這兩個對象調用{@code hashcode}方法都必須產生不同的整數結果。 * * @return a hash code value for this object. * @see java.lang.Object#equals(java.lang.Object) * @see java.lang.System#identityHashCode */ public native int hashCode(); /** * {@code equals}方法在非空對象引用上實現等價關係: * * 它是自反的:對於任何非空的引用值{@code x},{@code x.equals(x)}應該返回{@code true}。 * 它是對稱的:對於任何非空的引用值{@code x}和{@code y},{@code x.equals(y)}應該返回{@code true};反過來也應該一樣{@code y.equals(x)}返回{@code true}。 * 它是可傳遞的:對於任何非空的引用值{@code x}、{@code y}和{@code z},如果{@code x.equals(y)}返回{@code true},{@code y.equals(z)}返回{@code true},則{@code x.equals(z)}應返回{@code true}。 * 它是一致的:對於任何非空的引用值{@code x}和{@code y},多次調用{@code x.equals(y)}應該一致的返回{@code true}或{@code false},前提是在{@code equals}比較的對象信息沒有被修改。 * 對於任何非空引用值{@code x},{@code x.equals(null)}應返回{@code false}。 * * * 對於任何非空的引用值{@code x}和{@code y},當且僅當{@code x}和{@code y}引用同一對象,此方法返回{@code true}【此時{@code x==y}的值是{@code true}】。 * * 注意,通常需要重寫{@code hashcode}方法,以便保持{@code hashcode}方法的一般約定,它聲明:相等的對象必須有相等的哈希代碼。 * * @param obj the reference object with which to compare. * @return {@code true} if this object is the same as the obj * argument; {@code false} otherwise. * @see #hashCode() * @see java.util.HashMap */ public boolean equals(Object obj) { return (this == obj); }
可以看到: 1. 默認的hashcode方法是個本地方法,也就是對象的內存地址。 2. 而equals方法則是直接使用了==操作符,也是比較內存地址。這樣看來,equals和hashcode是一樣的。
而根據註釋所說: 1. 只要equals返回true,那麼hashcode一定相等。 2. 只要equals返回false,那麼hashcode一定不相等。3. 重寫了equals方法,也要重寫hashcode方法,因爲相等的對象必須有相同的哈希碼。
嘗試一:僅僅重寫equals
package com.demo.tools; public class User { public static void main(String[] args){ User a = new User("A"); User b = new User("A"); System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.hashCode() + "," + b.hashCode()); } private String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public boolean equals(Object obj) { User user = (User) obj; return this.name.equals(user.name); } }
輸出:
可以看到,值比較成功了,可是hashcode不一樣,這裏的hashcode仍然是內存地址。這就違反了約定。
嘗試二:重寫equals和hashcode方法
package com.demo.tools; public class User { public static void main(String[] args){ User a = new User("A"); User b = new User("A"); System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.hashCode() + "," + b.hashCode()); } private String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public boolean equals(Object obj) { User user = (User) obj; return this.name.equals(user.name); } @Override public int hashCode() { return this.name.hashCode(); } }
輸出:
可以看到,值和hashcode一致了,地址不一樣很正常,因爲這是兩個對象。
嘗試三:hashcode相同,equals就相等嗎?
public static void main(String[] args){ User a = new User("Aa"); User b = new User("BB"); System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.hashCode() + "," + b.hashCode()); }
輸出:
可以看到,兩個不同的字符串,hashcode可能一樣。
源碼一:Map裏的equals方法和hashcode方法
public boolean equals(Object o) { // 地址一樣,返回true if (o == this) return true; // 不是Map的子類,返回false if (!(o instanceof Map)) return false; Map<?,?> m = (Map<?,?>) o; // 大小不同,返回false if (m.size() != size()) return false; try { // 遍歷Map內容,一一比較 Iterator<Map.Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Map.Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); // HashMap裏允許存儲null值 if (value == null) { // 對方不存在此key,返回false if (!(m.get(key)==null && m.containsKey(key))) return false; } else { // 都有此key,但是值不一樣,返回false if (!value.equals(m.get(key))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true; }
這裏能回答一個問題:Map中的對象是自定義的,要不要重寫對象的equals方法?
看源碼我們能知道,默認Map裏的equals方法,在內部僅僅調用了value的equals方法,默認比較的是內存地址。在調用Map的equals方法時:
1. 默認一一比較對象的地址,也就是說,兩個Map中每一組比較的對象,都引用同一個地址,那纔算兩個Map相等。
2. 如果我們業務規定了相等的判定,那麼默認的則不符合實際需求,所以要重寫value的equals方法,不然調用Map的equals方法可能不符合預期,既然重寫了equals,那麼也要重寫hashcode了。
public int hashCode() { int h = 0; Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; }
不難看出,Map的hashcode方法是把內部元素的hashcode加在一起並返回。
源碼二:List的equals方法和hashcode方法
public boolean equals(Object o) { // 本身比較,返回true if (o == this) return true; // 非List的子類,返回false if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator<?> e2 = ((List<?>) o).listIterator(); // 當兩個迭代器的hasNext都爲true的時候,進行比較 while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); // 這裏很巧妙,利用三目運算符完成值的比較 if (!(o1==null ? o2==null : o1.equals(o2))) return false; } // 當任意一方hasNext爲true,返回false;當雙方都爲false,返回true return !(e1.hasNext() || e2.hasNext()); }
基本邏輯和Map的一樣,都是遍歷元素進行比較。hashcode也是一樣。
public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; }
源碼三:String的equals方法和hashcode方法
public boolean equals(Object anObject) { // 地址一樣,返回true if (this == anObject) { return true; } // 類型必須是String if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 比較兩個String的長度 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 遍歷比較 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
hashcode
private final char value[]; private int hash; // Default to 0 public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; // 這裏並不是把元素的hashcode加在一起,而是把元素的ASCII碼加在一起 for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
總結
不管平時有沒有重寫過equals和hashcode方法,看了Map和Set的源碼就可以知道重寫的必要性。因爲Set也是基於Map實現的(是一個key不一樣,value全一樣的Map),所以只用Map舉證即可。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
首先值的位置是根據hashcode決定的,然後判斷值相等的條件是equals爲true,hashcode相等。其它都爲不相等,有一種情況就是equals爲true,hashcode不相等,如果插入Set集合,就不能達到去重的效果:
package com.demo.tools; import java.util.HashSet; import java.util.Objects; import java.util.Set; public class Main { public static void main(String[] args) throws Exception { Set<Foo> set = new HashSet<>(); Foo a = new Foo("A", 12); Foo b = new Foo("A", 13); set.add(a); set.add(b); set.forEach(e -> System.out.println(e.name)); System.out.println(a.equals(b)); System.out.println(a.hashCode() + "," + b.hashCode()); } static class Foo { private String name; private Integer age; public Foo(String name, Integer age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Foo foo = (Foo) o; // 這裏我們認爲name一樣,則相等 return Objects.equals(name, foo.name); } } }
equals爲true,但是hashcode不一樣:
所以,如果重寫了equals,一定要重寫hashcode,以避免不必要的問題。
小技巧:IDEA裏可以自動生成兩個方法
快捷鍵:Alt+Insert
標準equals和hashcode,值得借鑑
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Foo foo = (Foo) o; return Objects.equals(name, foo.name) && Objects.equals(age, foo.age); } @Override public int hashCode() { return Objects.hash(name, age); }