Java中hash可以认为是唯一编码、摘要值,不同对象的计算方式不同。实质上将任意长度的输入,通过散列算法,变成固定长度的输出,输出值便是hash(散列)值。
/** java.lang.Object **/ /** This is typically implemented by converting the internal address of the object into an integer **/ public native int hashCode();
/** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string **/ private int hash; // Default to 0 public int hashCode() { // 此hash为字符串刚创建时初始化值为0的值 int h = hash; if (h == 0 && value.length > 0) { char val[] = value; // 基本方法就是用现有的hash值乘以31,再加上当前字符的unicode值(十进制) for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } // 计算完毕以后给字符串设置计算后的hash值用来缓存 hash = h; } return h; }
private final int value; public int hashCode() { return Integer.hashCode(value); } public static int hashCode(int value) { return value; }
HashMap是使用到Hash,也是面试中被提问到最多的一种,其实现的原理就是散列+数据。因为存储的数据是键值对,根据计算键的散列值计算出值应该存放的下标,从而实现查找效率为O(1)。
/** java.util.HashMap **/ // 最大容量,值为正整数二进制30位为1的,十进制是1073741824 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认载荷系数 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 带参构造器 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 带参构造器,只保留关键业务代码 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity > MAXIMUM_CAPACITY) { initialCapacity = MAXIMUM_CAPACITY; } tableSizeFor(initialCapacity); } // 计算比传入值更大的最接近的2^n + 1的值作为HashMap初始数组大小 static final int tableSizeFor(int cap) { // 以防传入的值正好是2^n值 int n = cap - 1; // 以下操作是将传入值二进制位中从左到右第一个1及后续位全部置1来求得极限2^n值 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; // 如果极限值超过了2^30 + 1(也就是1 << 30),则用最大值,如果小于,则加1凑够1*10^n return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
/** java.util.HashMap **/ // 充分散列 static final int hash(Object key) { int h; // 计算出key的hash值(如上),然后将二进制hash值的高16位进行无符号向右移动16位,之后这两个int类型的值做异或操作 // 其次这里的.hashCode(),如果是个私有对象的话,需要重写hashCode()和equals()方法,否则会取到对象的虚拟内存地址,下面有讲 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
n % hash(key) == (n - 1) & hash(key)
。/** java.util.HashMap **/ // 内部维护的列表 transient Node<K, V>[] table; // .put(key, value)方法(只取部分代码) final V putVal(int hash, K key, V value, ...) { // 缓存HashMap中的列表 Node<K, V>[] tab; // 缓存目标列表下标的对象 Node<K, V> p; // 计算后列表的长度和目标索引值 int n, i; // 判断内部列表是否为空或者长度为0,满足任一进行扩容(扩容方法在下面),并缓存内部列表到tab,缓存列表长度到n。注意:如果HashMap只初始化而未填充时,填充第一个键值时table==null,会进入这里,同时重新计算临界值threshold。 if ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length; } // 计算目标索引值,并判断在列表中是否已有使用,如果没有使用直接把key和value放进去,如果有了则需要解决Hash冲突 if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null); } else { // 当前列表索引的位置不为null,检查当前key和put的key是否相等,相等则做更新操作。 // 不相等的话需要检查这个值是什么类型的,如果是红黑树,则做树的插入或者更新;如果不是那就是链表,遍历链表是否有key一致的值,有则更新,无则新增,新增的时候需要检查是否超过8个,超过8个就需要构建红黑树(Java 8后支持)。 // 这里面还有个问题,就是在判断key是否一致的时候,使用的是.hash方法和.equals方法,如果key的类型是私有对象,没有重写equals和hashCode方法就会造成永远无法对相同的对象进行更新或者获取(因为获取的是Object的hashCode方法,即虚拟内存地址),这样大量的put会造成内存泄露,最后导致内存溢出(OOM)。 // K k; // if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) } }
table.length(当前列表的长度) * 设置的载荷系数
计算而出。// put()->putVal,摘取部分代码 final V putVal(int hash, K key, V value, ...) { // more code... if (++size > threshold) { resize(); } }
// 只摘取部分方法 final Node<K,V>[] resize() { // 缓存列表 Node<K,V>[] oldTab = table; // 缓存旧列表容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 缓存旧临界值 int oldThr = threshold; // 初始化新列表容量和临界值 int newCap, newThr = 0; if (oldCap > 0) { // 如果原数组就大于等于最大限制,就不允许扩容了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 后续扩容时计算新数组大小和临界值,只需要将二进制位向前推一下即可(在保证int不溢出的时候) else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 只有赋初始值的第一次put的时候才会走到这里,因为初始化时计算的临界值其实是列表的最大长度而不是真的临界值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 完全没有赋初始值的第一次put走这里,用的是默认大小列表长度16和临界值12 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 赋初始值的第一次put会计算临界值的具体数值,后续扩容在上方 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 重新给临界值赋予正确的数值 threshold = newThr; // 下面就是给列表扩容后重新计算hash值重新规划
// 列表长度为16时,与数值8和9分别按位与 1000 & 1111 = 1000 1001 & 1111 = 1001 // 列表长度为15时,与数值8和9分别按位与,则会产生碰撞 1000 & 1110 = 1000 1001 & 1110 = 1000
// 链表切换树的最大长度 static final int TREEIFY_THRESHOLD = 8; // 链表切换树最小列表长度 static final int MIN_TREEIFY_CAPACITY = 64; // 摘取部分代码 final V putVal(int hash, K key, V value, ...) { // 这部分代码是table为空扩容和目标索引为null时操作,见上面计算索引代码 // more... // 初始化一个"指针"e和key值 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) // 红黑树更新或添加成功后会返回null e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 这里是链表操作 else { // 遍历链表p的时候统计着有多少个 for (int binCount = 0; ; ++binCount) { // 因为p是链表对象,所以要遍历p,e就是链表的下一个,如果下一个为空,则将数据填充进去 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果当前链表的个数大于或者等于TREEIFY_THRESHOLD(因为从0开始,所以要减1) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 链表转为树 treeifyBin(tab, hash); break; } // 如果遍历的以后发现链表中的key和put的key相同,那就是更新操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 如果不想等也不为null的时候,因为e指向下一个,那么p就需要等于e,然后再次循环的时候,e就指向下下个 p = e; } } // 如果找到了需要填充的地方,把新值填充进去就可以了,除了TreeNode if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // 链表转树也有条件的,必须列表长度大于64的时候,否则就是扩容列表(精简代码) final void treeifyBin(Node<K,V>[] tab, int hash) { int n; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); }