HashMap原理
hashMap中重要参数:
/** * table 数组默认长度 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * Table数组的最大长度 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 扩展因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 链表树化阙值,表示在一个node(Table)节点下链表长度大于8时候,会将链表转换成为红黑树 */ static final int TREEIFY_THRESHOLD = 8; /** * 红黑树链化阙值:表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * 最小树化阈值,当Table所有元素超过该值,才会进行树化 */ static final int MIN_TREEIFY_CAPACITY = 64;
hashMap实现原理
hashMap主要是通过table[]数组 + 链表的方式来实现的,具体结构如下所示:
1、put方法
①对key的hashCode()做hash运算,计算index
②如果没碰撞直接放到bucket里
③如果碰撞了,以链表的形式存在buckets后
④如果碰撞导致链表长度大于等于TREEIFY_THRESHOLD,就把链表转换成红黑树
⑤如果节点已经存在就替换key对应的value值
⑥如果bucket的数量大于 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 时就需要扩容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table数组为空的时候先进行数组的初始化操作 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // resize()方法初始化数组,或达到DEFAULT_LOAD_FACTOR时扩展数组 if ((p = tab[i = (n - 1) & hash]) == null){ //i = (n - 1) & hash用来计算key在数组中的位置,通过hash值与数组长度减1做与运算 //数组长度为2的幂,n-1转成2进制刚好全为1,hash和n-1做运算,得到的值不会大于数组最大下标 //当该下标值为空时,新建Node,并且将next设置为空 tab[i] = newNode(hash, key, value, null); } else {//该数值位置不为空时,判断是否为key值是否相等 //判断方法:先判断key的hash是否相等,hash值不等的时候再使用equals方法 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) //如果当前Node类型为TreeNode,即此时为红黑树,往红黑树中添加节点。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//如果不是TreeNode,则就是链表,遍历并与输入key做命中碰撞。 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //如果当前Table中不存在当前key,则添加。 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) //超过了TREEIFY_THRESHOLD,将链表转化为红黑树 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//找到相同的key,则为更新 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; }
2、resize()扩容或初始化方法
resize()方法对HashMap进行初始化,或者超过扩容阀值时,对原有的数据进行拷贝移动到新的链表上,扩容大大小,采用左移1位,即容量翻倍
新的计算key在newTab位置时:e.hash & oldCap == 0 使用原有的位置,与 (n - 1) & hash 的值是一样的,同理 e.hash & oldCap != 0 时新位置为 j + oldCap 与 (n - 1) & hash 是一样的,自己可以计算一下,这里的n为扩容后的大小
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { //保存旧的table,方便后面操作 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; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的数组长度左移1位,且在最大值和最小值之间,threshold也左移1位 newThr = oldThr << 1; // 即容量和扩容阀值都乘2 } else if (oldThr > 0) // 容量为0,扩容阀值不为0,容量设置为扩容阀值 newCap = oldThr; else { // 容量和扩容阀值都为0时,默认配置 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 若新的阙值为0 就得用 新容量* 加载因子重新进行计算 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //开始对新的hash表进行相对应的操作 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //该位置有值时才进行移动处理 oldTab[j] = null; if (e.next == null) // 该位置只有一个元素,重新计算hash值的位置,放入 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //如果在旧哈希表中是树形的结果,就要把新hash表中也变成树形结构 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 该位置是链表,处理链表位置挪动 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { //遍历当前Table内的Node 赋值给新的Table next = e.next; if ((e.hash & oldCap) == 0) { // 原索引 if (loTail == null)
//找到第一个元素,后续链表的其他元素放在,该元素的next,放入newTab时只需放head loHead = e; else loTail.next = e; loTail = e; } else { // 原索引+oldCap if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { // 原索引放到bucket里面 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { // 原索引+oldCap 放到bucket里面 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
3、get方法
在判断key是否相等时,总是先判断hash值是否相等,相等时在使用equals是否相等,这里主要是因为:hash值相等equals不一定相等,equals相等,hash值一定相同,hash值比较比equals快,所以先使用hash值相比,hash值不同的直接比较下一个,这样效率更快,也保证了正确性
①.对key的hashCode()做hash运算,计算index;
②.如果在bucket⾥的第⼀个节点⾥直接命中,则直接返回;
③.如果有冲突,则通过key.equals(k)去查找对应的Entry;
④. 若为树,则在树中通过key.equals(k)查找,O(logn);
⑤. 若为链表,则在链表中通过key.equals(k)查找,O(n)。
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//数组不为空时,通过hash计算在数组中的位置 if (first.hash == hash && // 检查第一个Node 节点,若是命中则不需要进行do... whirle 循环 ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode)//树形结构,则对树进行检索 return ((TreeNode<K,V>)first).getTreeNode(hash, key); do {//链表结构,遍历链表 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
4、HashMap中的一些常见问题
①树化阀值TREEIFY_THRESHOLD = 8,而将树表为链表的阀值UNTREEIFY_THRESHOLD = 6,中间有个差值7可以防⽌链表和树之间频繁的转换,比如有个长度为8的链表,在该位置频繁的插入删除,插入删除,那么就会一直进行树和链表的来回转换
②HashMap是线程不安全的,在多线程中使用ConcurrentHashMap
③HashMap是通过空间来换取时间,以达到快速获取查找元素;需要将数据尽量的放在数组上,减少链表的长度,而在数据在链表的位置是通过 hash & (n - 1) 来获取,所以只要hash值是足够散列的,那么通过hash值计算出的位置也是正态分布在数组上
hash值的计算:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将生成的hashcode值的高16位于低16位进行异或运算,这样得到的值再进行相与,一得到最散列的下标值
一般使用String作为HashMap的key,因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象,获取对象的时候要用到到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,String类已 经很规范的覆写了hashCode()以及equals()方法。
④负载因子是可以修改的,也可以大于1,但是建议不要轻易修改
ps:没有分析红黑树相关代码