jdk1.7 HashMap源码初探

HashMap的数据结构

HashMap有个Entry<K,V>[] table属性用来存放最终的key-value,Entry是HashMap的内部类,table是一个Entry数组,初始是空的。只有执行第一次put的时候才会初始化Entry大小。

新建HashMap对象

HashMap共提供了四个构造方法,其中最常用的是无参构造方法,四个构造方法分别如下:

无参构造

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

其中初始大小为1<<4即16,扩容因子为0.75。扩容因子用于触发map扩容,详见后文。

传入一个已有的map对象

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

指定map初始大小

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

指定map初始大小和扩容因子

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

前面三个构造方法最终都是调用最后一个构造方法,这个构造方法只是指定了容量大小(capacity),和扩容因子(loadFactor),并对入参进行合法性校验。init方法是个空方法。此时table是空。threshold是扩容触发值,即达到这个值就触发扩容,详见后文。

put操作

完整的put方法

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    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;
}

第一次put

此时table是空的,所以需要初始化数组大小,大小取的是初始化时指定的扩容触发值(capacity )默认16

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
     int capacity = roundUpToPowerOf2(toSize);

     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
     table = new Entry[capacity];
     initHashSeedAsNeeded(capacity);
 }

其中roundUpToPowerOf2方法用于控制扩容大小必须是2的乘方。初始化分如下两步:
1.先新建一个指定大小的Entry(HashMap的内部类)
2.初始化hash掩码值(为后面扩容时提供判断是否需要进行重新hash,没有细看)

初始化之后获取key的hash值,然后再根据hash值获取要存放该key-value的下标

int hash = hash(key);
int i = indexFor(hash, table.length);

如果key是null,会调用putForNullKey方法插入key为null的Entry。所以HashMap的key是可以为null的

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

然后调用addEntry方法插入数据(由于是第一次put,所以Entry<K,V> e = table[i] 是null,不会进入for操作)

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

由于第一次put,无需扩容,所有直接调用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++;
}

说白了就是new了一个Entry对象并放入下标为刚才获取的下标下,看下Entry的构造函数

final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
   value = v;
    next = n;
    key = k;
    hash = h;
}

很简单,就四个属性,除了key,value外,还有一个hash值,以及它的nextEntry,说明这是一个链表结构。
至此第一个元素就已经put好了

无需扩容

大致同第一次put,只是少了一开始的初始化数组大小的操作。另外,如果这时put的key的hash值后获取到的下标所在元素已经存在了,则需要进入刚才没有进去的for循环进行替换操作

 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是否存在,如果存在则替换value值。判断依据是1.是否是同一个对象,2.调用key的equals方法,返回是否为true。
如果key不存在,则继续调用createEntry添加entry。如果该小标已经存在对象,则将原来存在的Entry放到自己Entry的next的属性里,即插入了链表的头部(头插法)。

需要扩容

大致同上,主要区别在于addEntry方法里的if语句块里

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

由此可见,HashMap扩容的触发条件是,当前已经插入的对象数量超过了需要扩容的数量(threshold),且要插入的下标有值。我之前会疑惑如果别的元素都插了好多了就这个下标没元素,这样岂不很不科学?其实key的hash值是能够保证正态分布的,无须担心。扩容完成后需要对key进行重新hash并获取新的下标(毕竟数组大小变了),其余同上。下面详细介绍扩容逻辑

扩容

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

resize大致有如下几个步骤:
1.首先判断已有的table数组大小有没有超过int的最大值(2^31),所以HashMap是不能无限制存东西的。
2.然后再新建一个新的大小的数组(原有大小的两倍)。说白了就是扩大了一倍。
3.然后调用transfer方法对已有的Entry数组进行重新hash(因为数组大小不一样了,所以同一个key的hash值也不一样了,所以要重新hash,不然就get不到了)
4.最后将重新散列后的entry数组赋给table,并重新修改扩容触发阈值
接下来详细说下transfer方法:

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

transfer方法也很简单,遍历table中的每个Entry,然后再遍历每个Entry的链表。然后对链表上的每一个Entry的key进行重新hash,存入新的table中。

get操作

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值获取table下标,然后再遍历该链表的每一个Entry,如果key相同则返回。(极端情况下,如果链表太长,会影响get的效率,毕竟遍历链表用的for循环啊。所以要设置合理的扩容因子,默认的0.75一般还是不要改的好)

jdk1.7扩容造成死循环分析

假设一个map有如下几个Entry
原有的Entry
扩容之后table数组大小变成4,两个线程同时执行扩容操作,线程一执行完transfer方法的e.next地方时被挂起,此时next是key7
在这里插入图片描述
此时线程二执行完整个扩容操作,扩容完成后的table结构如下
在这里插入图片描述
线程一继续执行,此时e是key:3,next=e.next=null(因为线程2已经完成了重新hash,将key3的next置为null,如图)。
线程1开始运行,继续进行扩容
1.此时e是key3,next是key7(刚才已经执行到这然后被挂起的)。线程1新建的newTable与线程2新建的newTable不是同一个对象,所以此时线程1新建的newTable下标为3的元素还是空的。继续下面的逻辑,将key3头插到下标为3的table中,key3的next为空(因为此时newTable下标为3的元素还是空),由于next是key7不是null,继续循环。
2.此时e是key7,由于线程2已经把key7的next置为了key3,所以此时next是key3,继续下面的逻辑,将key7头插到小标为3的table中,key7的next被线程1又置一次为key3,由于next是key3不为空,继续循环。
3.此时e是key3,next为空(见第一步),将key3头插到下标为3的元素中,key3的next被置为key7.但此时的next是null,所以循环结束,此时线程1的tabla结构如下

在这里插入图片描述
当get一个key时,如果该key被hash到下标为3这个元素,且不存在于map中,比如key15,程序就会遍历下标为3的Entry链表,先找到key3,然后遍历key3的next key7,然后再找key7的next key3,由于一直找不到key15,所以就会一直循环下去,就陷入了死循环。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值