jdk7 HashMap解析

本文详细解析了HashMap的构造方法、put操作以及扩容机制。在构造方法中,初始化容量和负载因子被设定,并确保它们的合理性。put方法首次插入时会初始化表格,通过hash函数计算key的散列值并插入,处理冲突。扩容时,新容量为原容量的2倍,使用transfer方法将旧表数据迁移到新表。同时,文章还介绍了modCount用于快速失败机制,确保并发安全性。
摘要由CSDN通过智能技术生成

一、构造方法

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
                                           
      //当初始容量太大,大于了允许的最大值时,使用最大值                                     
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        //判断加载因子必须是大于0,而且必须是数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();
}
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//初始化时将一个Map集合放入新创建的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    inflateTable(threshold);

    putAllForCreate(m);
}
 

二、put方法

public V put(K key, V value) {
    //当第一次放入元素时,开始做初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
   //这里是得到通过key计算出来的hash值,这个hash值通过
   //位移运算和hashseed进行位运算得到     
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    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++;
    addEntry(hash, key, value, i);
    return null;
}
private void inflateTable(int toSize) {
    int capacity = roundUpToPowerOf2(toSize);
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
 

1、roundUpToPowerOf2

获得大于给定容量最接近的2的次幂。比如,tosize为7 经过roundUpToPowerOf2后得到8。

原理:先将(number - 1) << 1,通过左移1位,让number变大后,再调用highestOneBit找到最终的值。

Integer.highestOneBit(获取小于给定i的最大2的次幂)

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

案例,比如现在i为20

(1)右移1位

i:      0001 0100

i>>1:0000 1010

|:    0001 1110

(2)右移2位

           0001 1110

i>>2:0000 0111

|:    0001 1111

(3)右移4位

           0001 1111

i>>4:0000 0001

|:    0001 1111

(4)右移8位

           0001 1111

i>>8:0000 0000

|:    0001 1111

(5)右移16位

            0001 1111

i>>16:0000 0000

|:     0001 1111

执行i - (i >>> 1),结果为16

i:        0001 1111

i>>>1:0000 1111

-:        0001 0000

2、hash

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

将key的hash值计算出来然后做位运算,看代码知道位移了20、12这些,为什么要移动这么多,其实核心思想就是避免出现太多了hash冲突,你想哈,如果不位移这么多,那么计算出来的hash值大多数都一样,因为高位都是0,所以这样就会导致一个问题就是hash冲突太多,链表太长,所以位移位数多了以后,尽量将避免hash冲突。

3、indexFor

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

不妨带进来一个数算一遍,假设 h 的二进制表示是1101 0110,数组的长度是8,二进制就是0000 1000,这时先进行length - 1的操作,得到0000 0111,这时再与 hash 进行&操作时,可以得到0000 0110,即十进制的6,而 HashMap 的容量,即数组的长度永远都是2的次方,也就是说,table.length的二进制表示永远都是一个1,其余都是0的状态,例如2的4次方16是0001 0000,5次方32是0010 0000,那也就是说明,table.length - 1得到的值永远都是前一半都是0,后一半都是1,这种结构再与 hash 进行&操作时,得到的结果就和hash % table.length一样了。为了避免hash冲突,因此要在计算hash值时,尽量散列开。
此处也解释了为什么在初始化hashmap容量时必须要获取到大于指定size的最小的2次幂。

4、putForNullKey

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

上面这个方法当key==null的时候调用,HashMap允许传入一个null作为key,至于为什么这样做,可能为了支持更好,而且如果是null的key,那么默认放在第一位,也就是数组为0的位置,那么这里会出现一个疑问就是,当你放置null key的时候,第0个位置已经被占用了,那么怎么办,这个时候就会存在第0个位置的链表上。
代码可以看出for循环中是从数组的第一个位置开始循环的,也就是说key = null的数据是放在数组为0的位置上或者数组为0的链表上;上面的这个方法是要返回一个值得,如果说我们添加key = null的数据的时候,这个null = key已经有了,那么会替换这个新的值,然后返回之前的值,所以HashMap的put方法是有返回值的,如果返回值不为空,则原来的HashMap中已经存在了这个key,并且已经覆盖了新的值,并且将旧值返回,如果返回null,则Hashmap中没有这个值存在。

5、addEntry

同一个Entry上的链表上采用头插法新增新的Node

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容的大小为原来数组长度的2倍,比如当前长度16,扩容后就是32
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
 //创建一个Entry存放我们添加的元素
    createEntry(hash, key, value, bucketIndex);
}
 

三、扩容

//这个newCapacity默认是扩容前的2倍,

void resize(int newCapacity) {
    //首先声明一个Entry数组用来存放原来的数组信息
    Entry[] oldTable = table;
    //得到原来的数组长度,然后判断扩容的大小是否已经达到了最大的长度,
    //如果大于了数组的最大长度,那么就设置阈值为最大数组的长度,则下次无法再扩容了
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //声明新的数组信息
    Entry[] newTable = new Entry[newCapacity];
    //数据的元素转移,就是讲oldTable转移到newTable中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}
//扩容的核心方法,就是讲原来的数组复制到新的数组中
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //这里使用了2层循环来进行数组的复制,为什么要使用2层循环呢?
    //因为hashmap是一般的数组结构,在数组元素上的单向链表结构,所以如果发生了数组
    //的扩容,需要两层循环来操作
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            //hashSedd的判断
            if (rehash) {
                //使hash更散列一些
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            //通过hash计算出来的下标,
            //在新的数组中赋值给原来数组的next
            //其实就是新的数组下标引用了原来的下标数据的引用地址            
            e.next = newTable[i];
            //将本次元素与原来解除关系过后,将引用变成原有的地址
            newTable[i] = e;
            e = next;
        }
    }
}
 

1、initHashSeedAsNeeded

该方法用来判断需不需要生成hashseed

当 capacity的值大于Holder.ALTERNATIVE_HASHING_THRESHOLD时,就生成一个hashseed,让hash算法的散列性,hashseed 的作用,让所得到的hash值更加散列,jdk8已经废弃。

Holder内部类代码如下

在这里插入图片描述

2、transfer

扩容迁移图示如下

在这里插入图片描述

多线程扩容的问题

假设线程2扩容运行到Entry<K,V> next = e.next这行代码时阻塞,线程一完成扩容

 线程二继续运行第一次循环

四、modCount(Fast-Fail机制)

表示每次对map的entry进行修改操作时都会对modCount做自增。

用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException

五、有用的文章

(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析_王伟的博客-CSDN博客_hashmap1.7和1.8的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值