HashMap 1.7源码解析

HashMap 1.7源码解析

1.介绍

1.描述

HashMap在我们平常开发中使用非常广泛,本文就从JDK1.7 分析HashMap相关源码(后续再加上1.8). 在JDK1.7HashMap底层是由数组+链表实现的,每次在插入数据的时候,会根据key来计算对应的Hash.使用各种位操作将Hash值转换成对应的数组下标,根据下标来找到数组(Entry)对应位置.如果当前位置对应的Entry对象不为空,则以头插的方式将数据插入到链表中.如果为空的话,直接将数据插入到Entry数组中.

2.线程安全问题

HashMap非线程安全的.在多线程的情况下,HashMap扩容的时候,会生成一个新的数组,将之前数组上的数据转移到新的数组上.在这个过程中可能会造成循环链表.所以在使用HashMap的时候最好是指定容量.一是为了防止多线程带了的问题,二是为了减少扩容带来不必要的损耗

3.图解

在这里插入图片描述

2.源码解析

1.创建HashMap,初始化阈值和负载因子

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

 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();
}

HashMap在初始化的时候,如果传入了相应容量负载因子就是用传入的,否则使用默认的. DEFAULT_INITIAL_CAPACITY 默认值 16 DEFAULT_LOAD_FACTOR 默认 0.75f,init()LinkedHashMap中有具体实现

2.put

public V put(K key, V value) {
    //判断数组是否是空
    if (table == EMPTY_TABLE) {
        //如果为空 初始化数组
        inflateTable(threshold);
    }
    //对key为null的处理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    //根据hash值获取数组的下标
    int i = indexFor(hash, table.length);
    //根据下标获取对应的Entry对象,遍历链表,判断是否有重复的key,如果有将value替换,返回之前对应的value值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判断key是否存在...
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //如果key不重复
    
    //记录修改的次数(快速失败(fail—fast)会使用到)
    modCount++;
    //存入到Entry中,下面分解
    addEntry(hash, key, value, i);
    return null;
}
1.inflateTable初始化Entry数组
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize 确保数组的容量 >=2的n次幂
    int capacity = roundUpToPowerOf2(toSize);

    //阈值 = 容量*加载因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    //该方法可以在启动时传入参数,修改hashSeed,干涉hash值的生成
    initHashSeedAsNeeded(capacity);
}
1.roundUpToPowerOf2 数组的容量 >=2的n次幂

方法主要是,让数组的容量 >=2的n次幂.比如:当我传入toSize= 1, capacity=2;toSize= 3, capacity=4;toSize= 16, capacity=16.就是这样的对应关系.

2.initHashSeedAsNeeded hashSeed(hash种子)
final boolean initHashSeedAsNeeded(int capacity) {
    //判断hashSeed 是否等于0(默认为0)
    boolean currentAltHashing = hashSeed != 0;
    //检查VM是否启动(true) 并且 容量 >= Holder.ALTERNATIVE_HASHING_THRESHOLD
    boolean useAltHashing = sun.misc.VM.isBooted() &&
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //将currentAltHashing   useAltHashing 做异或操作
    boolean switching = currentAltHashing ^ useAltHashing;
    //如果switching == true 
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}


综上分析 switching 值由 hashSeed 和 Holder.ALTERNATIVE_HASHING_THRESHOLD 值决定

1.hashSeed 作用
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

hashSeed 在HashMap出现了三个次(地方):一次初始化 一次initHashSeedAsNeeded方法中 一次hash方法hashSeed 决定hash值的生成

2.Holder.ALTERNATIVE_HASHING_THRESHOLD初始化位置
// Holder.ALTERNATIVE_HASHING_THRESHOLD  初始化
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
private static class Holder {
    static final int ALTERNATIVE_HASHING_THRESHOLD;
    static {
        String altThreshold = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction(
                "jdk.map.althashing.threshold"));
        int threshold;
        try {
            threshold = (null != altThreshold)
                ? Integer.parseInt(altThreshold)
                : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
            // disable alternative hashing if -1
            if (threshold == -1) {
                threshold = Integer.MAX_VALUE;
            }
            if (threshold < 0) {
                throw new IllegalArgumentException("value must be positive integer.");
            }
        } catch(IllegalArgumentException failed) {
            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
        }

        ALTERNATIVE_HASHING_THRESHOLD = threshold;
    }
}
//

​ 从代码中可以看出,判断在启动参数上是否加了jdk.map.althashing.threshold=XX 的信息,如果加了(不为空)就会将其转换为int赋给threshold,最后将threshold 有赋值给 ALTERNATIVE_HASHING_THRESHOLD.如果没有加入该启动参数(或者加了启动参数为复数),threshold 就会等于ALTERNATIVE_HASHING_THRESHOLD_DEFAULT 也就是( Integer.MAX_VALUE (2147483647)).

​ 显然我在启动的时候并没有加上 -D jdk.map.althashing.threshold ,所以Holder.ALTERNATIVE_HASHING_THRESHOLD=2147483647
在这里插入图片描述
initHashSeedAsNeeded作用:默认情况下,当容量>=Integer.MAX_VALUE或者在启动参数(-D jdk.map.althashing.threshold)传入一个非零的值,switcing才会为true,hashSeed才会改变。假如说你觉得HashMap中hash算法分布不够散列.那么你可以自己传入参数,干扰hash值的生成。

2.putForNullKey key为空的情况
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

putForNullKey中,可以看出HashMap会把key==null的数据放入数组为0的位置。获取数组的第一个位置,然后遍历数组对应的链表,如果存在的话key==null的情况,就将现有的value替换原有的value,并且返回原有的value。如果不存。就执行addEntry,以头插的方式将数据放入链表(后面详细解析)。

3.indexFor 根据hash值以及数组长度,获取当前key在数组中下标
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

​ 在上一步的hash方法中算出了key对应的hash值,在这里根据hash值数组长度(length)使用与(&)操作获取key在数组中的下标,这个操作的作用类似于计算中的%(求模)。源码中也写提到:length必须为2的非零幂,在roundUpToPowerOf2 方法得到确保,也就是数组初始化长度为2的非0次幂

4.遍历数组对应的链表,判断是否存在key重复
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;
    } 
}

​ 根据上一步获取的数组下标得到链表的头节点遍历链表中的数据。如果存在key,hash值相等,那么就会覆盖value值,并且返回之前的value值

5.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);
}

首先会判断size是否大于等于阈值以及table当前下标是不为空。当我们初始化HashMap(new HashMap())时,没有传入默认参数。第一次put的时候,size=0threshold初始化HashMap的时候是等于初始化容量,但是在第一次put的时候会初始化table,所以threshold=16*0.75=12
初始化

​ 上图我们是根据HashMap调用默认构造方法第一次put数据来分析的。threshold=12,第一次put数据时候,整个table数组都是空的只是初始化了长度(16)。显然以上条件,都是不成立的(PS:此处是以默认构造方法并且第一次put数据以此条件分析的)。从上面我们可以得到阈值=容量*负载因子

​ 上述方法如果过后会执行createEntry,创建Entry对象以头插方式插入到链表

1.resize 扩容
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);
}

​ 从上面分析可以知道,当size大于等于阈值并且当前下标(根据hash计算出来的)对应的table(数组)不为空,此时HashMap就会扩容,并且每次在之前数组长度*2(2 * table.length)。

​ 首先会判断之前的容量是否等于最大的容量,如果等于阈值就等于Integer最大值,并返回。不等于则创建一个创建一个新的数组大小是原来的两倍,执行transfer(扩容具体方法),table指向newTable,重新计算阈值

1.transfer 将之前table(数组)的数据转移到新的数组上
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            //获取e指向的Entry
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            //e指向扩容后的下标对应的newTable
            e.next = newTable[i];
            //将e放入数组中,头插
            newTable[i] = e;
            //将next赋给e
            e = next;
        }
    }
}

initHashSeedAsNeeded方法:默认情况下,当容量>=Integer.MAX_VALUE或者在启动参数(-D jdk.map.althashing.threshold)传入一个非零的值,switcing才会为true。rehash才会等于true ,所以一般情况下都是fasle详细initHashSeedAsNeeded描述在上面有讲解

PS:在多线程的时候此处可能会出现循环链表

​ 通过for循环遍历之前的table,然后通过while遍历数组上的链表。这样可以获取HasMap中所有的数据,然后根据每个Entry的hash值和新的长度(扩容后的数组长度)求出新的下标(i),再将逐个元素转移到新的数组上。

transfer
根据上述图片分析,假设原来table长度为4,新扩容后的table为原来的2倍即8。在table上下标为1的位置开始(应该是从0的位置开始遍历,这里笔者为了方便演示)为e,e指向next,然后根据新的容量求出e在newTable中的下标。接着e指向求出下标对应的位置(刚开始newTable[i]=null),然后将e放到数组中(头插)。最后将e指向table中的下一个节点next,继续迭代。

2.createEntry 创建新的Entry对象
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++;
}

​ 这里的代码就比较简单了,先取出e(当前下标位置对应的Entry),然后创建一个新的Entry对象指向e,并插入e对应位置,依然使用的是头插法

3.get

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

​ 首先判断key是否是null,如果是空的话就执行并返回getForNullKey,否则根据key从getEntry获取到Entry对象,最后判断返回的是否为空,来返回null或者Entry对象的value

1.getForNullKey key为空的情况
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

​ 在前面的putForNullKey 方法中提到过,HashMap会将key为null的值存到数组的第一个位置(table[0]),因此只需遍历table[0]下面的链表来获取到对应的value

2.getEntry 根据key获取Entry
 final Entry<K,V> getEntry(Object key) {
     if (size == 0) {
         return null;
     }

     int hash = (key == null) ? 0 : hash(key);
     for (Entry<K,V> e = table[indexFor(hash, table.length)];
          e != null;
          e = e.next) {
         Object k;
         if (e.hash == hash &&
             ((k = e.key) == key || (key != null && key.equals(k))))
             return e;
     }
     return null;
 }

​ 使用key获取对应的hash值,根据hash值以及数组的长度(table.length)来获取到在table中的下标。遍历数组对应下标的链表。判断,找出对应的key并返回,没有找到就返回null。

4.remove 快速失败

1.问题

在使用HashMap迭代移除元素的时候,如果自带的map.remove()方法会可能出现ConcurrentModificationException异常,但是使用 迭代器中的remove() 方法就不会出现问题,这是为什么呢?

2.分析

首先我们看看ConcurrentModificationException在HashMap中位置吧,两次都出现在抽象内部类HashIterator nextEntry remove方法中
concurrentModificationException
构造方法:在里面对expectedModCount进行了复制
HashIterator​ 由此分析出现异常的原因就是
modCount != expectedModCount
modCount想必大家都见过,在put方法出现过,每次put一个值都会modCount++ 。不仅如此,而且在putForNullKeyremoveEntryForKeyremoveMappingclear都有modCount++modCount的作用就是记录HashMap中增删改(变更)的次数,每次+1。

HashIterator有三个内部类继承,ValueIteratorKeyIteratorEntryIterator分别来看看
在这里插入图片描述
​ 仔细一看这不正是我们在使用迭代器获取key value 下一个Entry对象的相应实现吗,就是说我们每次在迭代的时候都会去比较modCount != expectedModCount

1.HashMap.remove方法

hashmap-remove
遍历链表移除对应元素,modCount++

2.Iterator.remove方法

iterator-remove
可以看出依然是调的HashMap.this.removeEntryForKey方法,但是在后面有expectedModCount = modCount,将改变后的modcCount赋给expectedModCount,那么下一次迭代的时候判断就是相等的,就不会出现异常。

3.结论

综上分析: HashMap在要迭代的之前,初始化迭代器调用HashIterator()并赋值expectedModCount = modCount。然后每次迭代的时候判断modCount != expectedModCount。使用HashMap自带的remove方法,只会删除数据expectedModCount不会还原。Iterator的remove调用HashMap中的方法还加上了expectedModCount = modCount,如此以来下次迭代判断的时候不会出现问题。

HaspMap之所以要这样,我的理解是:HashMap本来就不是线程安全的,所以他要尽可能的避免在获取数据的同时修改数据,特意加上了这种弥补机制(快速失败)。

3.HashMap相关面试题

待定。。。

4.总结

在分析HashMap的源码中,使用了大量的位操作:数组容量的确认,计算hash值,根据hash值确认数组下标等等。就是这些操作也同时决定了一些东西,数组的容量为什么是>=2的n次幂,数组为什么每次扩容2倍(我想都可能和位操作有关系)。使用HashMap的时候也应该尽可能避免扩容,最好给定指定的容量和负载因子。因为在多线程的情况扩容可能会造成循环链表,而且老数组的数据需要一个个的移到新数组上,开销是比较大的。

如果哪个地方写的有问题还希望大家毫不吝啬指出来,谢谢啦!

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值