java之HashMap详解

概览

HashMap的继承关系

这里写图片描述

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

public class HashMap<K,V>
    extends AbstractMap<K,V>

    implements Map<K,V>, Cloneable, Serializable { }

HashMap实现了Map接口,是一个存储键值对映射的集合,线程不安全,无序,键和值可以为null。

底层数据结构

这里写图片描述

HashMap的底层维护着一个Entry数组,即所谓的hash表,表中元素是Entry链表的第一个节点,链表中存储的是hash冲突的元素。
Java8后Entry改名为Node,并且当链表的容量达到一定值的时候转换成红黑树

源码解析

HashMap的静态属性值

//hash表默认容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 
static final int MAXIMUM_CAPACITY = 1 << 30;

//装填因子,默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//将冲突链表转为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;

//由红黑树转链表时,数节点的阈值, 小于这个阈值才能转为链表
static final int UNTREEIFY_THRESHOLD = 6;

//能将链表转为红黑树的最小hash表容量。应该至少4倍于 TREEIFY_THRESHOLD
//即只有当 冲突链表节点数大于等于8 且 hash表容量达到64时才将链表转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

put()方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

这里直接调用了 hash() 方法计算 key 的hash值 并调用 putVal() 方法

计算hash

static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

keynull ,其hash值为0.

h = key.hashCode()计算出 key 的hash值,h >>> 16 是因为计算出的hash值是32位的,将h无符号右移16位,再与原值异或运算。得到的新hash值的高16位较h的原值不变,原值的低16位和高16位进行了异或运算。

这么做的主要目的在于,当hash数组较小时,hash值的所有bit位都参与了运算,防止hash冲突太大。

putVal()详解

//hash: 当前key的hash值, onlyIfAbsent:是否覆盖重复值,hashMap 里面是false,evict:没用。。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    //首先是创建变量,tab赋值为HashMap底层数组,p是当前数组元素,n 数组大小
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //初始时table肯定为null
    if ((tab = table) == null || (n = tab.length) == 0)
        //由于初始时table为null,所以先做一次扩容操作
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //i = (n - 1) & hash 表示当前hash在数组中的下标,相当于取模运算
        //如果当前位置为空,则新建一个Node节点放入数组
        tab[i] = newNode(hash, key, value, null);
    else {
        //当前位置不为空,表示出现hash冲突
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //hash值相同,key值相同表明要插入的值与当前值相同,直接将当前值赋给e
            e = p;
        else if (p instanceof TreeNode)
            //p instanceof TreeNode 说明当前位置的hash冲突已经达到一定程度,链表转换成了红黑树
            //将当前值作为红黑树节点插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //hash冲突较少,Node还是以链表形式存在
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //这里就是把Node加到链表尾部
                    p.next = newNode(hash, key, value, null);
                    //注意这个binCount,当其值大到一定程度时,链表转为红黑树
                    //默认TREEIFY_THRESHOLD为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表转为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                //在遍历链表过程中发现相同的值,直接返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //e != null 说明存在相同的值,根据 onlyIfAbsent 选择是否覆盖
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) //在hashMap 中新值会替换旧值
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //操作记录+1
    ++modCount;
    //判断数组大小是否达到阈值,进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);//不管。。。 LinkedHashMap 中有用到
    return null;
}

putValue() 小结: 当向 HashMap 中添加新的值时:
1. 首先计算 key 的 hashCode
2. 如果数组为空,则先扩容
3. 检查 key 是否已存在于数组中,如果不存在直接添加节点,转到 6
4. 如果已经存在key,则在冲突链表或者红黑树中寻找是否存在相同的 Value,如果不存在则将此结点加到链表末尾或者RB树中,转到6
5. 如果Value也已经存在,则覆盖原值
6. 检查添加节点后是否需要扩容
7. 结束

resize() 扩容操作

final Node<K,V>[] resize() {
    //记录旧的数组
    Node<K,V>[] oldTab = table;//1
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//2
    int oldThr = threshold;//3
    int newCap, newThr = 0;//4
    if (oldCap > 0) {// 5
        if (oldCap >= MAXIMUM_CAPACITY) {
            /*
            如果数组旧的容量已经达到最大容量,将阈值也设为最大值
            此时不在进行数组2倍扩容,直接无限增加结点数量
            */
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /*
        如果数组还没有达到最大容量,则进行2倍扩容,扩容时还要判断是否达到最大容量
        如果达到最大容量则转到第8步
        oldCap >= DEFAULT_INITIAL_CAPACITY 说明已经进行过扩容,不再是初始容量
        */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //扩容同时增大新的阈值
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 6 initial capacity was placed in threshold
        newCap = oldThr;
    else {               //7 第一次resize时运行
        newCap = DEFAULT_INITIAL_CAPACITY; //初始默认容量16
        //初始默认装填因子0.75,因此默认阈值为 16*0.75 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // 8
        //如果运行该if, 表示扩容2倍到达上限或原数组大小小于16
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        //重新申请大小为newCap的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
   //下面是resize() 后迁移节点的代码
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果当前位置只有一个节点,则直接迁到新的数组,然后重新计算index
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果节点是红黑树,则转到split方法,split方法对于节点新数组中的位置计算与下面一样
                else if (e instanceof TreeNode)
                    ((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 {
                        next = e.next;

                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }

                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);

                    // e.hash 相较于 oldCap 高位为0,则沿用旧的下标值
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 高位为 1, 则新的下标值等于重新计算后加上旧的容量
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

如何计算节点在新的hash表中的位置:

e.hash & oldCap 判断用来计算hash值的高位是否为1。
这个1决定着节点在新的数组中的位置,这是因为数组扩容即 oldCap << 1 右移了一位
举个栗子,假设原数组容量为 4 (只要是2的幂就行),即: 100
hash值为 5,(101) 的节点在原数组的下标为: 
5 & (4-1) 
=   101
  & 011 
=   001;
数组扩容后,容量为8 (1000), 节点在新数组下标为 : 
5&(8-1) 
=  101
 & 111
=  101;
新的下标正好等于 旧的下标 001 加上 旧容量 4(100) ,即 101. 
上面是hash & oldCapacity = 1 的情况
当 hash & oldCapacity = 0 时自然容易算出新的下标和旧的下标相等。

Resize() 小结
HashMap 在扩容时:
1. 检查数组容量是否已经达到最大允许容量值, 如果达到了就把阈值也调整为最大int值,不再进行扩容。
2. 如果没有达到最大容量值,则进行两倍扩容,同时更新阈值。
3. 如果扩容之后达到的最大容量值,则把数组长度和阈值设置为最大值。
4. 数组的最大容量为 1 << 30.

上面的扩容后计算新的下标值设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上面可以看出,JDK1.8不会倒置.
—– http://www.importnew.com/20386.html

get()方法

//get操作
public V get(Object key) {
    Node<K,V> e;
    //先计算key的hashCode, 然后调用 getNode()方法
    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;

    /*
     * 这里先进行了一系列判断
     * 1. 判断hash表是否为null 或 空 
     * 2. 判断 key 有没有在hash表中
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {

        //如果查找到的第一个节点就等于key 则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果第一个节点不等于 key 则表明出现hash冲突
        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;
}

小结

通过上面的分析我们知道HashMap的基本数据结构是数组、链表和红黑树,初始时处理hash冲突使用链表,当链表规模达到8时,转化成红黑树。同时每当数组元素达到阈值会进行扩容,扩容后的数组是原来的两倍。因此在插入请求较多时,最好指定初始容量以获得更好地性能。

这里再说一个比较有意思的算法

这里的内容来源于另一个博客,地址忘了。。。

HashMap在使用指定容量初始化时使用了一个很精妙的算法来保证初始化后的数组容量大于等于指定容量的2的幂中的最小数。
举个栗子:假如传给HashMap初始化的容量cap = 13, 那么HashMap实际的初始容量为
16 = 2^4 > 13
这样既能保证容量够用,又方便以后计算元素位置 (length - 1) & hash, 相当于对2的幂取余操作。
方法如下:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

举例:

cap = 9  16
cap - 1  第一步结果8
00000000000000000000000000001000    8
00000000000000000000000000000100移1
00000000000000000000000000001100    或运算 结果
00000000000000000000000000000011移2
00000000000000000000000000001111    或运算 结果

00000000000000000000000000001111    右移 4 8 16没用全是0结果还是这个15
最终 +1   16

分析下大点的数 12345678
00000000101111000110000101001110  12345678
00000000101111000110000101001101  -1结果   12345677
00000000010111100011000010100110移1
00000000111111100111000111101111  或运算 结果
00000000001111111001110001111011移2
00000000111111111111110111111111  差不多了在移0就没了都是1,+1不是肯定是2的倍数了

至于为什么最终只右移到16位,我猜想是因为最大的容量 MAXIMUM_CAPACITY = 1 <<< 30
16+8+4+2+1 = 31 > 30 ,所以右移31位已经可以保证移位后的n的低位全是1,高位全是0了。

最后这里有篇文章介绍 HashMap 在多线程环境下的不安全情况:
http://www.importnew.com/22011.html

参考:
http://www.importnew.com/20386.html
http://www.importnew.com/22011.html
http://www.cnblogs.com/skywang12345/p/3310835.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java HashMap是一个散列表,用于存储键值对的映射关系。它实现了Map接口,根据键的HashCode值来存储数据,具有非常快速的访问速度。HashMap允许一条记录的键为null,但是键是唯一的,即同一个键只能对应一个值。 例如,下面的代码展示了如何使用Java HashMap: Map<Integer, String> map = new HashMap<>(); map.put(1, "a"); map.put(2, "b"); map.put(3, "c"); 这段代码创建了一个HashMap对象,并将键值对存储在其中。键的类型是Integer,值的类型是String。通过put()方法可以向HashMap添加键值对。 此外,Java HashMap还提供了其他常用的方法,如get()方法用于通过键获取对应的值,remove()方法用于移除指定键对应的键值对等等。HashMap的内部实现使用了哈希表来实现高效的数据存储和访问。 需要注意的是,HashMap是非线程安全的,如果在多线程环境中使用,需要采取额外的措施来保证线程安全性。另外,在序列化和反序列化HashMap对象时需要特殊处理,可以通过自定义readObject()和writeObject()方法来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java HashMap](https://download.csdn.net/download/weixin_38588592/13705053)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [JavaHashMap 详解](https://blog.csdn.net/java1527/article/details/126850576)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值