Map相关源码分析

本文详细解读了HashMap的构造方法,重点介绍了put操作中如何处理hash碰撞,以及resize函数的逻辑。涉及了为何调整hash值、计算索引策略和判断何时转换为红黑树。同时对比了与其他Map(如LinkedHashMap,TreeMap,Hashtable)的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap

构造方法

  • HashMap有多个重载的构造方法,可设置map的容量、扩容因子以及传入map,保证了map的容量为2的次幂,默认的大小是16,其他带有参数的重载构造方法最终都是执行到这个方法中
	// map的默认容量16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
	
	public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // 让设置的容量转为2的次幂
        this.threshold = tableSizeFor(initialCapacity);
    }

核心API

  • put:添加元素;通过计算key.hash & size - 1 得倒一个在0-size-1的范围索引,然后判断当前位置是无元素,则创建一个节点添加到该位置;如果是有链表,则查找当前链表中是否包含key,存在则替换value,否则添加到链表尾部,然后对链表进行判断,长度达到7且数组长度未达64,则进行数组扩容,否则就需要对链表转为红黑树;如果是有树,则将节点添加到红黑树中,由于红黑树上添加元素,则操作完后,需要对树进行平衡操作,进行左旋右旋等操作(后面会出红黑树文章讲解)
    public V put(K key, V value) {
    	// 重点1:hash(key),获取key的hash值,并异或上了hash>>>16,
    	// 让hash值的二进制高16与低16位进行异或,降低了hash碰撞的概率
        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;
        if ((tab = table) == null || (n = tab.length) == 0)
        	// resize() 重新计算数组大小
            n = (tab = resize()).length;
        // 判断1:当前索引无节点,直接添加节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 判断2:当前位置存在链表,且有相同key,则替换值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 判断3:当前位置存在的是树节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 判断4:当前位置是个链表
            else {
                for (int binCount = 0; ; ++binCount) {
                	// 将新节点链到链表的尾节点上
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表达到7,则判断是需要扩容,还是转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 存在相同的key
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 当添加元素后,需要判断map容器是否需要进行扩容,
        // 这个判断依据就是初始化时,设置的扩容因子loadFactor,默认是3/4
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

考点1:为何在获取key的hash值,需要对key.hash>>>16位?当不进行右移运算,则会导致,只有hash值的低位与n - 1进行运算,这样很容易造成hash碰撞,所以就有右移hash值,使得高16位与低16位都参与跟n - 1进行运算,增大了hash的随机性,大大降低了hash碰撞概率

考点2:为何计算节点的索引需要hash & n - 1?因为我们知道集合的容量为2的次幂,所以在二进制中都是1多少个0,eg:16 -> 0001 0000,所以这么一个值和任何二进制数&计算出来的得到的值要么是0,要么是1,所以,就导致索引0和1的位置上出现很多元素,所以通过-1就会使n - 1变成0000 1111,这样& 计算的结果就交由hash的二进制决定,降低了hash碰撞的概率

考点3:为何判断链表转红黑树时,链表长度达7时,就需要进行扩容/转为树?可能是因为当链表过长时,在访问数据时,可能性能不佳;如果太短的话就会造成频繁的对数组进行扩容,还需要对链表进行拆链;以及链表太过于频繁的转为红黑树,都对性能上造成影响

考点3:当集合的大小达到loadFactory * cap时,需要对容器进行扩容,loadFactory默认值为3/4,当loadFactory越接近1,则hash碰撞率相对较大,反之

  • resize:重新计算容器大小;如果是第一次添加元素时,这时候才会在resize()中创建指定大小的数组;如果是已存在数组,则是将数组容量扩展到原来的2倍,然后再遍历原数组元素,如果是单个节点,则通过key.hash & newCap - 1获取节点位置;如果是链表,则将链表按key.hash & oldCap计算分别将得到0和1的节点分别串成链,0的链表存放到原来所在索引index下,1的链表存放到index + oldCap下;如果是树,则会对树结构进行拆分,按key.hash & oldCap计算分成0和1两个链表,如果长度达到7,则对链表进行转为树处理,然后将0的链表/树放到原先index位置,1的链表/树放到index + oldCap 的位置
    final Node<K,V>[] resize() {
            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;
                }
                // 判断表示对已存在的集合容量进行扩容2倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            // 判断初始化map使,设置了容器的容量
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            // 判断初始化map时,使用的是无参构造方法
            else {    
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 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;
            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)
                            newTab[e.hash & (newCap - 1)] = e;
                        // 如果当前位置是一个树结构,则进行拆分成子链表/子树,在存放到数组中
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        // 当前位置是一个链表,进行拆分成子链表,在存放到数组中
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                // 数组扩容是扩成原来的2倍,当从扩容后的数组中根据e.hash & (newCap - 1)计算
                                // 槽位时,将比原先的e.hash & (oldCap - 1)计算多出oldCap。
                                // 所以在数组扩容后,根据(newCap - 1)的二进制的最高有效,对应的hash二进制的相同位值的数值,
                                // 如果是0则不影响计算槽位值index,如果是1则需要将这些hash值,对应的链表移动到index + oldCap位置上
                                // 逻辑可通过草稿笔算验证
                                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);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

考点1:为何在扩容时,如果是链表,需要对链表进行key.hash & oldCap计算按结果0和1进行串链?在扩容后再通过key.hash & newCap - 1得到的索引与key.hash & oldCap - 1得到的索引不一致,就会导致取错value,举个例子说明,根据下面例子,了解了由于扩容,导致链表中有的元素计算槽位出现偏差,所以就将偏差的子链表更新到了正确位置上

oldCap = 16
key.hash     0000 0000 0101 0111 0010 0001 1100 0110
oldCap       0000 0000 0000 0000 0000 0000 0001 0000
oldCap-1     0000 0000 0000 0000 0000 0000 0000 1111
扩容前: &  
------->     0000 0000 0000 0000 0000 0000 0000 0110 由hash值低四位决定
newCap-1     0000 0000 0000 0000 0000 0000 0001 1111
扩容后 当hash值第五位为0时:&
---------->  0000 0000 0000 0000 0000 0000 0000 0110 由hash值低五位决定
当hash值第五位为1----------> 0000 0000 0000 0000 0000 0000 0001 0110
可见当扩容后计算索引的值取决于hash值参与运算的最高位(即第五位)的影响,如果为0,
则newCap - 1与oldCap - 1计算值一致,如果为1则比原来索引多16,
即多出来oldCap的大小
  • get:根据key获取value;通过key.hash & cap - 1得到index,然后,找到位置上的节点,是单个节点还是链表,还是树,都进行挨个节点比较hash和key都是否相等,然后取出value
  • remove:根据key移除对应value;通过key.hash & cap - 1得到需要删除的index,然后找到位置上的节点,是单个节点还是链表,还是树,都进行挨个节点比较hash和key都是否相等,单个节点就置空Node中的引用,并将当前位置置空;如果是链表,则不仅仅要置空Node,还需要将Node前节点与后节点链接;如果是树,同理需要对Node进行置空,但是由于是红黑树的特性,所以要移除节点后,需要对树进行平衡操作,进行一系列的左旋右旋等操作(关于红黑树后面会出一篇讲解,再次不进行叙述)
  • putAll:则是添加一个Map集合;就是对添加的map进行遍历,单独执行put(key,value)

考点:为何HashMap是无序的容器?
以下操作都会使得HashMap内部数据位置进行重组:
1、当链表过长时,会对容器进行扩容/将链表转为红黑树
2、容器扩容时,需要对链表进行拆分成子链表存放数组中
3、容器扩容时,会对树进行拆分成子链表/子树,重新放到数组中
4、当集合元素个数达到cap * loadFactory时,需要扩容

LinkedHashMap

  • LinkedHashMap是HashMap的子类,对自己的put、get方法都是复用父类的,唯一不一样的地方是,LinkedHashMap内部的节点不是Map.Node,而是LinkedHashMapEntry,它是Map.Node的子类,LinkedHashMapEntry内部维护一个一个前后的Node节点引用,以至于LinkedHashMap可以通过双链表的形势进行访问元素,并且达到一个有序的状态

考点1:LinkedHashMap底层数据结构?双链表+数组+红黑树
考点2:LinkedHashMap是一个可以按存储顺序遍历的有序容器

LinkedTreeMap

  • LinkedTreeMap对key有要求,首先不能为null,其次key必须是可比较类型Comparable,否则会抛NullpointerException和ClassCastException;由于key是可比较类型,所以LinkedTreeMap是一个key有大小顺序的,也可以是按添加顺序进行遍历的有序容器

考点1:LinkedTreeMap的key限制?1、不可为null;2、必须是Comparable类型
考点2:LinkedTreeMap底层数据结构?红黑树 + 双链表
考点3:由于key是可比较类型,所以可按key的大小顺序进行迭代;又由于容器中每个节点又构建一个双链表,所以也可按添加顺序进行迭代

Hashtable

  • Hashtable是一个线程安全的容器,内部是通过synchroinzed关键字修饰,它限定了value不可为null,底层是由数组+链表的结构,它确定节点所在数组中的位置,是通过key.hash % length它的key.hash就是简单的获取hash值,没有想hashMap中,为了降低hash碰撞,对hash值进行离散性处理,而且它的链表也没有设置长度

考点1:Hashtable是一个线程安全的,有synchroinzed关键字修饰,所以他的速度会相交其他的容器慢
考点2:底层的数据结构是数组+链表
考点3:它的value限定不可为null
考点4:由于它的设计未限制链表的长度,加上它也未处理降低hash碰撞,所以大大增加了hash碰撞的可能性,链表存在过长的风险,导致访问数据性能差

TreeMap

  • TreeMap它也是限定了key不能为null且是Comparable类型,所以可以按照key的大小顺序进行迭代,它的底层是红黑树结构

考点1:同样,key是可比较类型,所以不能为null
考点2:底层的数据结构是红黑树
考点3:只能按key的大小顺序进行迭代
考点4:由于是树结构,所以访问元素使用的是二分法查找

根据HashMap的使用频率高,则针对HashMap的源码进行分析,其他Map则只是分析了他们的特征

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值