HashMap(Java)
何为HashMap:HashMap是常用的数据结构,由数组和链表组合构成,可理解为链表构成的数组(注:jdk1.8之后如果链表长度达到阈值(默认为8),就会更改使用红黑树)
1.红黑树相关关键参数
//一个桶的树化阈值 //当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点 //这个值必须为 8,要不然频繁转换效率也不高 static final int TREEIFY_THRESHOLD = 8;想知道为何为8?//一个树的链表还原阈值 //当扩容时,桶中元素个数小于这个值,就会把树形的桶元素还原为链表结构 //这个值应该比上面那个小,至少为 6,避免频繁转换 static final int UNTREEIFY_THRESHOLD = 6;//哈希表的最小树形化容量 //当哈希表中的容量大于这个值时,表中的桶才能进行树形化 //否则桶内元素太多时会扩容,而不是树形化 //为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64;//当多个bin被映射到同一个桶时,如果这个桶中bin的数量小于TREEIFY_THRESHOLD(8)当然不会转化成树形结构存储;如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD(8) ,但是capacity小于MIN_TREEIFY_CAPACITY (64)则依然使用链表结构进行存储,此时会对HashMap进行扩容;如果capacity大于了MIN_TREEIFY_CAPACITY (64),则会进行树化。这里为何NTREEIFY_THRESHOLD = 6,而TREEIFY_THRESHOLD = 8,也是有依据的。
2.桶的树形化 treeifyBin()
/*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.*///将桶内所有的链表节点替换成红黑树节点final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;//若当前哈希表为空,或者数组table的长度小于MIN_TREEIFY_CAPACITY时,进行resize(),//这里需要注意两点:1.resize()中包括初始赋值,也包括扩容// 2.从源码中可以看出,hashMap不是有一个链表达到TREEIFY_THRESHOLD就会马上进行红黑树转换,在执行树形化之前,会先检查数组长度,如果长度小于64,则对数组进行扩容,而不是进行树形化。这是一定要注意的if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();//否则进行树化else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}}TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}table发生扩容的时候有两种情况:
一种是元素达到阀值了,即(Capacity:HashMap当前长度*LoadFactor:负载因子,默认值0.75f)。 一种是HashMap准备树形化但又发现数组太短(小于MIN_TREEIFY_CAPACITY),这两种情况均可能发生扩容。这种情况树形化其实是治标不治本。3.红黑树中添加元素 putTreeVal()
在添加时,如果一个桶中已经是红黑树结构,就要调用红黑树的添加元素方法 putTreeVal()。
4. 红黑树中查找元素 getTreeNode()
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value; }5.树形结构修剪 split()
HashMap 中, resize() 方法的作用就是初始化或者扩容哈希表。当扩容时,如果当前桶中元素结构是红黑树,并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6),就会把桶中的树形结构缩小或者直接还原(切分)为链表结构,调用的就是 split():
6.jdk1.7的头插法(Entry)和jdk1.8(Node)的尾插法
hashmap在java8之前是头插法:新来的值会插入到旧值的前面,新值的内存地址成为tableb表的值,原有的值就顺推到链表中去。java8之后使用的是尾插法:新来的值插到链表后面。但是头插法在并发的情况下,会引发死循环。因为我们都知道,hashmap再达到一定的容量时,会引起HashMap容器的扩容resize(),扩容后就会rehash(),为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变, hash公式:index = HashCode(Key) & (Length - 1)。而就是再rehash这个过程中,如果有多个并发线程进行rehash,就有可能造成rehash后的链表中存在环,(形成环的根本原因:就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上,如下)
当下一次要在这个链表中找一个不存在的值时,就会形成死循环,所以jdk1.8之后就使用了尾插法,但是,即使是这样,hashmap还是线程不安全的,如果要解决这个问题,考虑ConcurrentHashMap.这篇大佬的文章对JDK1.7Rehash()形成环的过程讲得较明白(个人认为)而JDK1.8之后,链表有红黑树的部分,但链表过程时,转换成红黑树,将原本O(n)的时间复杂度降低到了O(logn)。JDK1.8采取的尾插法在扩容时会保持链表元素原本的顺序,不会出现链表成环的问题,原因是扩容转移后前后链表顺序不变(JDK1.7扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。)
7.hashmap的默认初始化长度:16
默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16),这样做的最终目的就是:只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的,可以减少哈希碰撞,实现均匀分布。同时位运算是直接在内存中进行的,效率比取模运算高太多了
原代码中有如下:
原理:X % 2^n = X & (2^n – 1)
如下例子:
这篇文章写得太好了
8.JDK1.8 putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。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 {//如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//如果在遍历过程中找到 key 相同时直接退出遍历。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//最后判断是否需要进行扩容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;}9.JDK1.8 get()方法
public V get(Object key) {Node<K,V> e;//首先将 key hash 之后取得所定位的桶,如果桶为空则直接返回 null 。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;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。if (first.hash == hash && // always check first node((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;}8.解决hashmap的线程不安全
使用hashTable或者ConcurrentHashMap,但是因为hashtable并发度的原因基本上没啥使用场景了(hashtable的解决方法比较粗暴,直接在方法上锁,并发度很低,最多同时允许一个线程访问),所以主流都是使用ConcurrentHashMap。
总结
以上是生活随笔为你收集整理的HashMap(Java)的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Java中equals、==和hashc
- 下一篇: Java8新特性之函数式接口