Java的HashMap源码解析
作者:龙三丶
前言
以jdk1.8为例,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对是一个Node(jdk1.7叫做Entry)。后台是用一个Node数组来存放数据,这个Node数组就是HashMap的主干。
这里我们主要来分析HashMap的get和put方法。
put
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; //如果是第一次put,就进行数组的大小初始化,默认是16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据hash值,找到在数组中的位置,如果此位置没有值,就new一个新的node插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果数组该位置有值 else { Node<K,V> e; K k; //判断该位置节点的key是否和即将插入的key相等,相等就取出来等待覆盖 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) { //循环到最后一个节点,然后插入新节点(1.7是往头结点插入,1.8是往尾部插入) if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断插入后的该链表的长度,如果大于8,就转成红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //这里表示链表中某个节点的key与即将插入的key相等,就跳出循环等待覆盖 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //这里表示有节点的key与新的key相等,那么就覆盖 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //插入完之后,如果导致size超过了预设的阈值,就进行扩容(1.7是插入前判断,1.8是插入后判断) if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
扩容步骤:
1、创建一个原数组两倍大小的新数组,并且把阈值扩大一倍。
2、遍历原数组,进行数据迁移。分为红黑树和链表两种情况。
好了,扩容部分就不展开代码详细说明,接下来进入get方法,相较于put方法就没那么复杂了,且代码量也比较少
get
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //判断底层node数组是否为空及该hash值对应的数组位置是否有值 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //判断该数组的节点是不是我们需要的值,是就直接返回 if (first.hash == hash && ((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; }
注意:
1、HashMap底层就是用一个个的Node来存储单个数据,每个Node有hash值、key、value、及指向下一个Node的引用(next)。Node数组中就是所有链表的头节点。
2、当出现hash冲突的情况,原Node的next就会指向新插入的Node,也就是形成了链表。
3、每次扩容的长度必须是2的幂,因为,根据key的hash值计算出的数组索引应尽量不要重复,实现均匀分布,均匀分布的话大部分查找的数据都是以数组的形式查找,就不会蜕变成链表,而数组的查找效率比链表高很多。
4、影响扩容的因素有两个:数组的长度(DEFAULT_INITIAL_CAPACITY)和负载因子(DEFAULT_LOAD_FACTOR),当这两个相乘大于等于当前HashMap的Size时,就进行扩容
5、扩容在并发情况下可能会形成链表环,存在并发安全问题,这点需要注意
6、当链表的节点超过8个时,会转成红黑树,链表的时间复杂度为O(n),而红黑树为O(logn)
到此这篇关于Java的HashMap源码解析的文章就介绍到这了,更多相关HashMap源码 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!