HashMap源码

一:简介

1:存储格式(key,value)的方式,根据key的hashCode值存储数据。

2:允许key的值为Null。

3:是非线程安全,即任一时刻可以有多个线程同时写HashMap,在扩容的时候可能会导致数据的不一致,出现死循环,从而使CPU使用率达到100%,服务器宕机。

4:如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

    ConcurrentHashMap核心数据如table、value、链表都是volatile修饰,1.7使用分段锁啊,1.8使用CAS+synchronized实现。

二:数据结构

jdk8之前hashmap的存储是数组+链表的形式,jdk8以后以数组+链表/红黑树的形式存储

三:HashMap中的相关变量

1:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4

 默认初始化桶的数量(容量的大小) , 1转成二进制左移四位是10000 为16

2:static final int MAXIMUM_CAPACITY = 1 << 30;

 最大桶的值 1向左移动30位

3:static final float DEFAULT_LOAD_FACTOR = 0.75f

默认负载因子0.75 超过16*0.75=12后HashMap将进行扩容,桶的数量扩大2倍

4:final float loadFactor;

实际负载因子,可以new HashMap的时候传此值

5:static final int TREEIFY_THRESHOLD = 8

链表转成红黑树的阈值,在存储数据时,当链表长度 > 8时(需要结合MIN_TREEIFY_CAPACITY参数),则将链表转换成红黑树

6:static final int UNTREEIFY_THRESHOLD = 6

红黑树转成链表的阈值,当链表长度 < 6时,则将红黑树转换成链表

7:static final int MIN_TREEIFY_CAPACITY = 64;

 链表转红黑树所需的最小的桶(容量)阈值:即 当哈希表中的容量 > 该值时,并且链表长度 > 8时,才允许将链表转换成红黑树。否则,先尝试扩容解决链表过长的问题,而不是树形化

8:int threshold

扩容阈值(threshold),当哈希表的大小 >扩容阈值时(if (++size > threshold)  resize()),就会扩容哈希表(即扩充HashMap的容量),扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。扩容阈值 = 容量 x 加载因子。也是HashMap所能容纳的最大数据量的Node(键值对)个数

四:HashMap相关构造方法

// 可传参初始容量和负载因子,注意:此处的容量如果不是2的幂,会自动转成大于此数的最接近的2的幂  
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
         //如果大于MAXIMUM_CAPACITY那么就用MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //赋值扩容量,tableSizeFor方法生成的值是指定容量的值往上找最近的2次幂的数
        this.threshold = tableSizeFor(initialCapacity);
    }

     //默认0.75负载因子,容量可以传参  
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //默认0.75负载因子,容量16   
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
 
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

五:HashMap关键方法

put(K key, V value),hash(Object key),putVal(int hash, K key, V value, boolean onlyIfAbsent,  boolean evict),treeifyBin(Node<K,V>[] tab, int hash)

当两个node的hash值相同的时候,就会发生hash碰撞,这个时候就会判断两个node的key是否相同,相同的话覆盖,不相同则放到链表尾部,用Node中的next属性指向下一个Node。

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

   //获取key的hash,使用key的hashCode和h >>> 16(>>>无符号向右移动16位)进行异或运算
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

/**
     * 实现了map的put和其他相关方法
     *
     * @param hash key的hash值
     * @param key key值
     * @param value 需要put的value
     * @param onlyIfAbsent 如果true,那么不会覆盖已存在的值
     * @param evict 如果是false,那么代表此map处于创建模式
     * @return previous value, or null if none
     */

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //node数组临时变量
        Node<K,V>[] tab;
        Node<K,V> p; 
        int n, i;
        //第一次添加元素
        if ((tab = table) == null || (n = tab.length) == 0)

          //数组(hashmap)的长度
            n = (tab = resize()).length;
        //判断下一个数组位置是否有值,无的话则新增一个Node放到Node数组中,下标 i = (n - 1) & hash,n 就是HashMap中数组的长度默认为16,hash就是通过对象key产生的hashcode。也就是下标是通过数         //组长度-1 和 hashcode 按位与产生的。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

      else{

        Node<K,V> e; K k;
            //获取当前元素的hash值、key,与put的元素进行比较判断;
            //这个情况是hash值相同,key值也相同的情况,put的元素覆盖当前下标的元素
            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 {
                //程序走到这,说明链表第一个节点是hash值相同,key值不相同(或者i = (n - 1) & hash计算后的下标相同,比如负载因子是1.5 容量是4,那么下标0和下标4的会在同一个链表上)
                // 遍历当前链表,把当前的put的key-value封装成Node放到链表的最后(jdk1.8)
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 是因为 binCount 是 0 为第一个节点
                            treeifyBin(tab, hash);
                        break;
                    }
                    //判断链表中是否由hash值相同,key相同的node
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //链表存在hash值相同,key相同的node,将新value覆盖旧value
            if (e != null) { // existing mapping for key
                //老值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    //覆盖老值
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
  }
        ++modCount;
        if (++size > threshold)
             //创建一个比原来数组容量大两倍的新数组,遍历原来的数组,把原来数组上的元素重新按 
             //位与运算,放到新数组上。
            resize();
        afterNodeInsertion(evict);
        return null;
}

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //判断数组长度是否小于64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
             // 先扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        //大于64 转红黑树
            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);
        }

1:测试1

上面我们说到当两个node的hash值相同的时候,就会发生hash碰撞,这个时候就会判断两个node的key是否相同,相同的话覆盖,不相同则放到链表尾部
那么是否链表中值的hash是否都一样?答案不是的,如果数组长度-1和hash值与运算后产生了相同的值,此时即使hash值不一样,也会存放到同一个位置的数组上,即(n-1)&hash相同

为了方便这里使用1.5作为负载因子,初始化容量是4

public class TestHashMap {
    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>(4,1.5F);
        for (int i=0;i<6;i++){
            int h;
            Integer key =i;
            int hash = (h = key.hashCode()) ^ (h >>> 16);
            System.out.println("hashcode:"+key.hashCode());
            System.out.println("hash:"+hash);
            System.out.println("运算后下标:"+String.valueOf(3&i));

            map.put(i, i);
        }
    }

}

当程序进行到i=4的时候,此时是第五个数,由于key是0的时候 3&0=0,此时key是4 ,3&4=0,即它们都到了下标0这个位置,由下图还发一个规律,产生的下标值和数组的长度有很大关系,当长度为4的时候,产生的下标只和后2位有关,当长度为8的时候,产生的下标只和后4位有关,以此类推。。。。。。

然后判断它们的hash和key是否相同,相同则覆盖,很明显一个Hash是0,一个hash是4,再判断它们是否是红黑树,也不是,最后走到在下标0的位置创建了链表,node的值为0的指向了node值为4的位置。i=5的时候同理,node的值为1的指向了node值为5的位置

 

此时的数据结构

map的键值对

因此,负载因子越低,桶的数量越大,那么发生哈希碰撞的概率就越小,效率就越高,但是越浪费内存

2:测试2

是否链表长度大于8,就开始转红黑树?

为了方便这里使用9作为负载因子,初始化容量是2

看下面的例子,当程序走到 i=16,也就是第17次进来,此时HashMap的数据结构,2个桶的链表长度都已经到了8,但是由于桶的数量小于64,此时触发了resize()方法进行了扩容,而不是转红黑树。此时桶的容量变成了原来的2倍4,threshold也由原来的18变为了36

 

    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>(2,9);
        for (int i=0;i<18;i++){
            int h;
            Integer key =i;
            int hash = (h = key.hashCode()) ^ (h >>> 16);
            System.out.println("hash:"+hash);
            System.out.println("运算后下标:"+String.valueOf(3&i));
            if(i==16){
                System.out.println("size17");
            }
            map.put(i, i);
        }
    }

此时的数据结构

链表的数量已经大于了8(下标是0开始的)

进行HashMap扩容

i=17的时候,发现桶的数量已经扩容成了4,threshold变为了36

扩容后的数据结构,在i=16的就已经重新进行了hash运算,下标的位置也会变化,其实hashMap中的数据位置也可以看成当前的数据结构。

由此可见扩容是一个特别耗性能的操作,所以在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容

此时的数据结构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是Java中的一个常用的集合类,用于存储键值对的数据结构。它的底层数据结构是数组、链表和红黑树。HashMap可以存储null的键和值。下面是对HashMap源码的详细分析。 1. HashMap的特点:HashMap是无序的,不保证元素的顺序。它允许使用null作为键和值。HashMap不是线程安全的,如果在多线程环境下使用,需要进行额外的同步。 2. HashMap的构造方法: - HashMap():创建一个空的HashMap实例,默认初始容量为16,负载因子为0.75。 - HashMap(int initialCapacity):创建一个指定初始容量的HashMap实例。 - HashMap(int initialCapacity, float loadFactor):创建一个指定初始容量和负载因子的HashMap实例。 - HashMap(Map<? extends K, ? extends V> m):创建一个包含指定Map中所有键值对的HashMap实例。 3. HashMap的成员方法: - put(K key, V value):向HashMap中添加键值对。 - hash方法:计算键的哈希值。 - 扩容方法resize():当HashMap的大小达到阈值时,会自动扩容。 - 删除方法remove():根据键删除对应的键值对。 - 查找元素方法get():根据键获取对应的值。 4. HashMap的常见问题: - HashMap的容量为什么是2的幂次方?这是为了提高哈希函数的散列性能和数组的索引计算效率。 - 为什么负载因子默认是0.75?这是为了在保持较高的查找效率的同时,尽可能减少哈希冲突和扩容次数。 综上所述,HashMap是一种底层使用数组、链表和红黑树的存储方式的键值对集合类,并提供了一系列的方法来操作和查询数据。它的源码实现细节涉及到数组的扩容、哈希函数的计算、链表和红黑树的操作等。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [最详细的Hashmap源码解析](https://blog.csdn.net/qq_45830276/article/details/126768408)[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_1"}}] [.reference_item style="max-width: 50%"] - *3* [HashMap源码解析](https://blog.csdn.net/weixin_46129192/article/details/123287837)[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_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值