一篇带你吃透HashMap原理(附HashMap面试题夺命十三问)
作者:互联网
4.6 HashMap
4.6.1 HashMap存储结构
- HashMap的数据存储结构是数组+链表+红黑树。
- HashMap的初始容量为16,负载因子为0.75,扩容因子是2。
- 当链表的长度为8时,链表转化为红黑树,当红黑树的大小为6时,红黑树倒退为链表。
- 每次扩容后数据所在位置可能发生变化。
这里用一张图来表示hashmap的存储结构。
4.6.2 HashMap底层原理
HashMap最底层时一个存储链表类型的数组对象。
transient Node<K,V>[] table;
当我们创建了HashMap对象,当我们需要放入元素时,链表数组table才被初始化。
Map<Integer,String> map = new HashMap<>();
map.put(1,"唐僧");
这个时候,底层用key的hash值和数组的长度-1进行与运算(位运算),计算出键值对应该存放在数组的下标。
hash(key);
if ((p = tab[i = (n - 1) & hash]) == null)
如果数组对应下标不存在值
将key,value,hash封装成链表结点Node,放入数组的指定位置中
tab[i] = newNode(hash, key, value, null);
这个时候,数组就放入了我们的第一个链表对象了。
如果数组下标存在值,获取数组下标存在值(即链表)。
p = tab[i = (n - 1) & hash])
如果这个key是已经存在的,直接将value更换为传递进来的value。新建一个临时结点e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
V oldValue = e.value;
如果key不存在,判断链表是否是红黑树上的元素,若是,则在红黑树上进行增加元素操作。并指向临时结点e。
if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
若不是,则新建一个Node结点,并将新结点加入原链表后面。
e = p.next;
p.next = newNode(hash, key, value, null);
break;
如果结点数量等于8个,将链表转换为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
将e链表加入数组中后面
afterNodeAccess(e);
在最后,进行判断,如果元素个数达到12个,进行扩容resize
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
4.6.3 一张图说清HashMap put原理
4.6.4resise方法
HashMap 的扩容实现机制是将老table数组中所有的Entry取出来,重新对其Hashcode做Hash
散列到新的Table中
4.6.5 HashMap小知识
第一问: 为什么使用链表+数组:
- 为了解决hash冲突和提高存储效率,hash冲突是指根据key值确定hash值后,会出现两个不同的key值定位到数组的同一下标位置中。为了避免这个冲突,我们可以采用链表结构来使这两个键值对存储在这个相同数组下标所在位置。
第二问 我⽤LinkedList代替数组结构可以吗?
- 当然可以,因为LinkedList本身也是一种链表结构。但是链表查询效率太低。
第三问 那既然可以使用进行替换处理,为什么有偏偏使用到数组呢?
- 用数组效率最高,数组查询能力比链表高。HashMap是根据这个下标来确定元素存储位置的,在这方面数组能直接根据下标定位,而链表只能一个一个遍历。
第四问:那ArrayList
,底层也是数组,查找也快啊,为啥不⽤ArrayList?
- 因为ArrayList的扩容是1.5倍扩容,HashMap需要进行频繁的位运算,所以最适合HashMap扩容的机制是两倍扩容,位运算是2的次幂。而基本数组支持我们自定义扩容机制。
第五问:讲一讲HashMap的get/put过程。
put过程:
- 根据key获取hash值,定位到数组下标,查看是否有值。
- 若没有值,直接封装一个含有hash,key,value的Node结点放入(table[index])中。
- 若有值,判断是否key的值是否和原有的值相等。若相等,替换原有value。
- 若不相等,判断结构是否为红黑树中结构,如果是红黑树结构,调用其他方法增加到红黑树的元素。
- 若不是红黑树结构,新建一个封装了数据的结点并加入到原链表的后面。判断元素个数是否达到8个
- 若达到,链表转化为红黑树。
- 判断总元素个数是否达到最大容量的0.75。若达到,resize扩容。
- 最终存储到数组结构中。
get过程:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
- 对key值的hash值进行位运算,获取下标值。
- 检查第一个结点,如果正确,直接返回。
- 如果是红黑树结构,使用红黑树的检索方法。O(logn)
- 如果是链表结构,遍历链表,找到key值对应的结点。O(n)
- 如果都没找到,返回空值。
第五问:还知道哪些hash算法
MD4,MD5加密算法。
第六问:知道jdk1.8中hashmap改了什么吗。
- 数组+链表结构改为数组+链表+红黑树
- 优化了高位的hash算法。
- 扩容后,元素要么在原来位置,或者原来位置的移动2次幂。
第七问:说一下为什么会出现线程的不安全性
- 进行扩容时候,会导致进入新数组时候出现倒序的情况,也会在多线程时候出现线程的不安全性,尽量使用线程安全的ConcurrentHashMap。
第八问:为什么不一开始就使用红黑树,不是效率很高吗?
- 红黑树需要进行频繁的左旋,右旋,变色操作来保持平衡,单链表不需要。
- 当元素小于8个时,链表查询效率已经足够,还能保证增加结点速率。
- 当元素大于8个时,用红黑树来提高查询效率。但是新增结点速度变慢。
第九问:红黑树什么时候退化为链表
- 元素个数为6的时候退化,中间一个差值7防止红黑树与链表频繁转换。
第十问:HashMap在并发环境下会有什么问题,一般是如何解决的。
-
多线程扩容操作引起的死循环
-
多线程put导致数据丢失。
-
多线程get值为空。
-
加锁声明synchronized
-
使用ConcurrentHashMap,允许几个元素共用一把锁,提高效率。
第十一问:key可以是null吗,value可以是null吗
- 当然可以,但是key值只能有一个为null;value允许多个为null。
第十二问:一般用什么作为key值
一般使用String作为key值
- 字符串是不可变的,它的hashcode值在创建时就被缓存,不需要重新创建。
- HashMap获取对象的时候需要使用正确的equals()和hashCode(),String正确的对它进行了重写。
第十三问:用可变类当Hashmap1的Key会有什么问题。
- 可以put,当可变类发生更改时,无法get。
标签:面试题,hash,HashMap,数组,链表,夺命,value,key 来源: https://blog.csdn.net/super1223/article/details/117201308