JDK1.7HashMap源码解析

本文详细剖析了JDK1.7中HashMap的内部实现,包括其数据结构(数组+链表)、装载因子、阈值、初始化过程以及resize扩容策略。在HashMap的put操作中,首次插入会触发数组初始化,并采用头插法避免遍历冲突。当元素数量达到阈值时,HashMap会扩容为原来两倍的容量,但在多线程环境下可能导致死循环的问题。通过预设容量和负载因子,可以有效避免HashMap在多线程下的扩容问题。
摘要由CSDN通过智能技术生成

JDK1.7里的HashMap采用的是数组和链表的数据结构
hashmap
首先,该类继承于AbstractMap实现规定的一些方法,Cloneable支持克隆,Serializable序列化
先从类的一些成员说起:

  • loadFactor
    装载因子,是用来衡量 HashMap 满的程度,计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是 table 的长度length。

  • threshold
    计算公式:capacity * loadFactor。这个值是当前已占用数组长度的最大值。过这个数目就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍

  • modCount 修改次数 保证数据一致性 防止写冲突

  • DEFAULT_INITIAL_CAPACITY 1<<4 aka 16

构造函数

提供四种构造方法
在这里插入图片描述
构造时,提供capacity和loadFactor双参构造,无参构造时,capacity默认是16,loadFactor是0.75f.且capacity有个最大值1<<30

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;
        threshold = initialCapacity;
        init();
    }

要注意,这时的构造并没有产生Entry[]数组,而是在第一次执行put元素时才产生Entry[]数组

put方法

public V put(K key, V value) {
//如果数组为空,会执行对数组的初始化操作
//为空的原因是该数组并不会在new HashMap()时就产生,而是在第一次
//执行put操作时才会产生 inflateTable()方法后面会讲
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
 //hashmap支持key为null的put操作
        if (key == null)
            return putForNullKey(value);
 //根据key计算出hash值 该hash()方法后面也会讲到
        int hash = hash(key);
 //根据hash值和数组长度取得一个下标 indexFor()方法后面也会讲到       
        int i = indexFor(hash, table.length);
 //该循环目的是遍历数组第i个下标的链表
 //目的是找到与key相同的Entry并覆盖旧的value 并且返回旧value       
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
 //如果是个新key则采用头插法加入到该链表中,该方法后面会讲到       
        addEntry(hash, key, value, i);
        return null;
    }

inflateTable(threshold)

该方法用来初始化Entry数组

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
		//找到大于该值的最小的2次幂方数 即12 -->16  31-->32 64-->64
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化阈值
        //根据前面算出来的capacity初始化Entry[]数组
        //而且前面算出来的capacity必须是2的次方数,所以Entry数组必须是2的次方数即2,4,8,16,32......1024......1<<30 为什么必须是这么大后面也会讲
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
//用来找到大于等于number的最小2的次方数
private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        //如果number大于最大容量了,就只用最大的容量就行
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    //Integer.highestOneBit(n)该方法用来找到小于等于n的最小2次方数
   //源码如下
    public static int highestOneBit(int i) {
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }
    //这个是怎么找到的呢,举例分析 17应该找到的是16
		//int在java中32位这里只写低8位    
    //17        0001 0001
    // >>1		0000 1000
    // |=       0001 1001
    // >>2      0000 0110
    //  |=      0001 1111
    // >>4		0000 0001
    // |=    	0001 1111
    // >>8		0000 0000
    // |= 		0001 1111
    //右移16位依然为0001 1111
    //再将其>>>1    0000 1111
    //用i减去该值	0001 0000即16 then retrun.
    //分析原理 该方法将形如001* ****这种数据转换成0010 0000这种数据
    //因为2进制中2的次方数只有一位为1其余位全为0 保留最高位的1其余位为0
    //则找到该最小值
    // roundUpToPowerOf2((number - 1) << 1))
    //先不管减1 如果num为15 <<1 翻倍为30找到小于或等于其的2次方数则为16 
    //                   16 <<1 为32 小于或等于其2次方数则为32但是num为16 要求得到的值应该为16 所以number - 1 then << 1 16 - 1 = 15 << 1 = 30 得到的值就为16 
     //为什么要 >> 1 >> 2 >> 4 ..... >> 16的原因是
     // 1 + 2 + 4 + 8 + 16正好是32 而int值为32位 保证了较大值也能计算出结果                

hash()

先说indexFor()方法

  static int indexFor(int h, int length) {
        return h & (length-1);
        //该方法时根据hash值找到数组中的下标
        //正常思路就是将hash值与数组的长度取余%就会得到0 - (length - 1)的值,但是%运算比较耗时,就用了&运算 原理分析如下
        // 例如hash 值为 18  0001 0010 数组长度为8
        //    0001 0010
        //&   0000 0111
        //=   0000 0111  7
        // 例如hash 值为 9  0000 1001 数组长度为8
        //    0000 1001
        //&   0000 0111
        //=   0000 0001  1
        //但是这种方法 上面这个例子来说 0000 1001得到的是1 而**** 1001得到的都是0000 0001这样hash碰撞就比较严重,因为高位并没有参与到运算中。所以为了避免这种碰撞,该类提供了hash()方法重新计算了一个hashcode,增加了散列性,而不仅仅只用key.hashcode()
    }
final int hash(Object k) {
//hashSeed初始值为0
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();
//将h 异或 key.hashcode()
//假设key.hashcode() 为 0001 0110 与0000 0000异或
// ^=       仍为0001 0110
//后面的操作是将高位都参与到低位的运算中,增加hash的散列性,为indexFor()提供支持
   
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

addEntry()

  void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
  void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

因为Entry对象里有key value next
头插法即 先取出Entry e = table[index]
table[index] = new Entry[key , value , e] 即可。
采用头插法时,插入是 O(1)的,遍历是O(n)的。
尾插法 插入O(n) 遍历也为O(n)
但是在put时会先遍历这个链表看这个链表里是否有相同的key的Entry,
所以其实无论头插法尾插法效率都差不多。

resize()

 void addEntry(int hash, K key, V value, int bucketIndex) {
 //此时会触发扩容 扩容后容量是原容量的二倍.
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
void resize(int newCapacity) {
//检查数组的大小是否是最大值,如果是,不支持扩容
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
//创建二倍大小的新数组
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
 //将旧数组复制并散列到新数组里       
        table = newTable;
   //改变引用     
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
 //将旧数组及链表散列到新链表里
 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

按照代码所示
先将rehash当作false处理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终复制完成,不难发现新数组中链表的顺序与原链表顺序相反。
在多线程下,假设两个线程操作时,都需要扩容,假设线程A扩容完成,线程B走到创建好e和next指针时停止
在这里插入图片描述
这种情况下B继续操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终形成了循环链表,也就是说当你对其链表再遍历的时候就会形成死循环。
那hashMap多线程下会出现死循环就无法使用了吗?
答案是否定的。避免死循环就是避免hashmap进行扩容。也就是说如果你需要30个数据去存储,那么你初始化数组的时候就先设定好容量,从而避免出现扩容。当然,jdk1.7的hashMap是没有解决这个问题的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张嘉書

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值