哈希表插入是在尾节点插入,因为需要遍历哈希桶内所有链表节点判断重复
哈希表的底层是由数组、链表和红黑树实现的,数组作为哈希桶。 桶内的结点个数小于8链表实现,大于等于8红黑树实现 Java中所有类都继承了Object类,该类中有一个hashCode方法,是C++实现的native方法,将key的内存地址转化为32位int数a,再对a右移16位得到b,将a与b异或得到c作为key的哈希值。得到的哈希值对哈希桶长度 - 1做&操作,结果就是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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 public native int hashCode () ;static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } 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 ) 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 ; }
可以看到,在执行putVal方法之前,会调用native方法hashCode得到哈希值,由于这个方法会让每一个不同的对象都得到不同的哈希值,如果我们希望两个内存地址不同但相同内容的数组的哈希值也相等,就必须重写hashCode方法,使得相同内容的数组的哈希值也相等。 Java为许多常用的数据类型重写了hashCode()方法,使它们只要内容相同哈希值就也相同,比如String,Integer,Double等。比如在Integer类中哈希值就是其int类型的数据。
1 2 3 4 public static int hashCode (int value) { return value; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int hashCode (int a[]) { if (a == null ) return 0 ; int result = 1 ; for (int element : a) result = 31 * result + element; return result; }
通常重写hashCode方法后还需要重写equals方法。这是因为对象通过调用 Object.hashCode()生成哈希值,由于不可避免地会存在哈希值冲突的情况 因此hashCode 相同时 还需要再调用 equals 进行一次值的比较,但是若hashCode不同,将直接判定两个对象不同,跳过 equals ,这加快了冲突处理效率。
1 2 3 4 5 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
第三行代码中,由于重写了hashCode方法,导致内容相同的数组具有相同的哈希值,于是判断p中的键是否等于传来的数组(比较地址)肯定是不等于的,于是执行key.equals方法。 因此,重写hashCode方法导致不同地址的对象具有了相同的哈希值后,为了让某些方法的底层实现将它们作为完全相同的对象,需要重写equals方法。
Object 类定义中对 hashCode和 equals 要求如下:
如果两个对象的equals的结果是相等的,则两个对象的 hashCode 的返回结果也必须是相同的。
任何时候重写equals,都必须同时重写hashCode 。
相关链接源码解读HashMap底层结构与实现原理之一–put、get方法大起底 底层原理_森森之火的博客-CSDN博客