为何重写 equals 就必须重写 hashCode 有这样一条定则——如果重写了 equals 方法,就一定要重写 hashCode 方法 。
这是因为,对象的 hashCode 是同对象的值相关联 的,如果两个对象相等,则两个对象的 hashCode 也应当相等。可是,Object 类的 hashCode 方法其返回值只与对象的地址相关联,因此,如果只重写 equals 方法,就会导致这样一种情况——两个 equal 的对象,其 hashCode 却是不相等的。这会导致在 HashSet,HashMap 之类的利用哈希表进行映射的数据结构出现问题。
HashSet,HashMap 的哈希函数遵循这样的逻辑——如果两个对象拥有不同的哈希值,则它们是必定不同的,如果拥有相同的哈希值(这是哈希碰撞 的情况,两个对象不相等,但其哈希值经过再次混淆后相同),则调用 equals 方法再判断是否确实为相等的对象。
我们编写 hashCode,没有必要强求一个对象必然要得到独一无二的 hashCode,这是不可能的,尽量减少碰撞即可。
注:==运算符比较的是两个变量的值 ,对于基本类型,则比较的是它们的值,对于引用类型,则比较的是它们存储的地址 ,因此,只有两个引用类型的变量(即使其类型不同也可以,比如一个子类的对象引用 和一个父类的对象引用指向一个子类的对象)指向同一个地址(或者说,堆里的同一个对象实例 )的时候==才返回真。
从 HashSet 的源码入手 当我们调用 HashSet 的 add 实例方法的时候,会发生什么?看看源码吧。
@Override public boolean add (E e) { return map.put(e, PRESENT)==null ; }
再看看 HashMap 的 put 方法。HashMap 使用的是数组——链表结构,如果某个链表长度大于阀值(8)且数组长度大于 64,则该链表将被转换为红黑树结构,否则会将数组的长度乘 2。不过判断的流程还是一致的——首先将元素的 hashCode 使用杂凑函数进行处理,确定其插入位置,如果插入位置为空,则直接插入,(对于数组——链表)否则遍历该位置的所有元素,对每一个元素进行判断;首先比较哈希值是否相等,如果相等,则判断其是否是同一对象,判断其是否 equal,如果是则说明是重复的 key,对该 key 进行处理,如果到最后都未找到,则将其插入尾端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
PS:HashSet 和 HashMap,IdentityHashMap 允许插入 null 或以 null 为 key。它的哈希将为 0。
PS:看看 IdentityHashMap IdentityHashMap 则是另一个极端。与 HashMap 不同,它只比较元素的内存地址(即 Object 的 hashCode 方法,或者说可以理解成==运算符)来判断其是否是同一个 key,而不关心是否 equal。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private static int hash (Object x, int length) { int h = System.identityHashCode(x); return ((h << 1 ) - (h << 8 )) & (length - 1 ); }public V put (K key, V value) { final Object k = maskNull(key); retryAfterResize: for (;;) { final Object[] tab = table; final int len = tab.length; int i = hash(k, len); for (Object item; (item = tab[i]) != null ; i = nextKeyIndex(i, len)) { if (item == k) { @SuppressWarnings("unchecked") V oldValue = (V) tab[i + 1 ]; tab[i + 1 ] = value; return oldValue; } } final int s = size + 1 ; if (s + (s << 1 ) > len && resize(len)) continue retryAfterResize; modCount++; tab[i] = k; tab[i + 1 ] = value; size = s; return null ; } }