HashMap源码分析

本文参考:http://blog.youkuaiyun.com/happy_horse/article/details/52316865

关于HashMap,你应该知道的几个知识点:

HashMap多线程的条件竞争;
不可变对象的好处;
为什么String, Interger这样的wrapper类适合作为键;

1、HashMap内部结构组成及实现

这里写图片描述
HashMap是基于哈希表(使用了拉链法解决Hash冲突的哈希表)的一个Map接口实现,类中具体的实现是基于数组和链表;存储的对象是一个键值对对象(Entry

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

链表节点Entry

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    ·····
    //其重写了equals()、hashcode()和toString()方法
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}

2、HashMap类

构造函数:

public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)

第二和三个构造函数都是调用了第一个函数。第四个函数不讲

public HashMap(int initialCapacity, float loadFactor) {
    //初始容量不能小于0,hashmap类默认初始容量为16
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    //初始容量不能超过最大值MAXIMUM_CAPACITY,其默认值为2的30次方
    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的构造函数之一,其他构造函数都引用这个构造函数进行初始化。
参数InitialCapacity指的是HashMap中table数组最初的大小,
参数loadFactory指的是HashMap可容纳键值对与数组长度的比值(举个例子:数组长度默认值为16,loadFactory默认值为0.75,如果HashMap中存储的键值对即Entry多于12,则会进行扩容)。
参数threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子),若table是空表时,其等于初始容量。
注意:
在构造函数中不会对数组进行初始化,只有在put等操作方法内会进行判断是否要初始化或扩容。

3、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是否是空数组,若是空数组,则调用inflateTable(int)方法对数组进行初始化,其输入参数为初始设置的InitialCapacity。

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

在构建table数组时,数组的容量必须是2的幂数(原因稍后解释),所以put()方法首先调用roundUpToPowerOf2(int)方法,判断初始值是否是2的幂数;若是,则使用这个初始值;若不是,则将其改为一个比初始值大的最小的2的幂数,用来保证table数组的容量永远是2的幂数。

在构建完数组后,将会判断存入元素的key值是否为空(这里说明了HashMap是允许存入对象的键值为null的),所有键为空的对象都会存入table[0]中。
若键值不为空,则调用hash()方法,结合对象的key的哈希值获取整个对象的哈希值,以最大程度的确保相同对象会有相同的哈希值,不同对象会有不相同的哈希值:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) 
    {
        return 
           sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

在获取对象的哈希值后,会调用indexFor(int h, int length)方法,来确定对象放在table数组的什么地方。

static int indexFor(int h, int length) {
    return h & (length-1);
}

我们知道对于HashMap的table而言,数据的分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素均匀分布呢?我们会想到取模,indexFor()方法相当于h对length取模。但如何保证元素的均匀分布呢?这就是系统要求table数组的容量必须是2的幂数的原因。如图:
这里写图片描述
从上面的图表中我们看到当length=15时,总共发生了8此碰撞(最后一列为数组中的位置),同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。
而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

在计算出对象存放在table数组中的位置后,将取出对应位置上的链表头元素,并循环单链表:

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

其中,hashMap通过对象的hash值,来获取其存放位置,如果存放位置有多个对象,则通过key.equals()方法来判断,是否是一个对象,是则覆盖其value值。这说明,HashMap通过hash()和equals()来保证对象的唯一性。

如果链表中不存在当前对象,则将当前对象插入到链表的头部。调用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);
}

在插入前,首先要进行容量判断,若当前table[]中的对象个数>=先前设定好的阀值时(threshold = 数组容量 * 负载因子),同时对象将要插入的位置已经有别的对象了时,将会对当前table[]进行自动扩容。新的容量为原来容量的2倍,扩容方法是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);
}

如果原来的table[]的容量已经是最大值MAXIMUM_CAPACITY,则不进行扩容,而是将阀值扩大到最大值MAXIMUM_CAPACITY;若table[]的容量不是最大值,则进行扩容;扩容后,需要重新对table[]中已经存储的对象进行再hash,使用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;
        }
    }
}

注意对每个已存储对象再hash后,其存储链表顺序与扩容前的链表顺序相反。
容量扩充完毕后,再计算其在table[]中的存储位置,将其插入到存储位置链表的头部。

4、get()方法:

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

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

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

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是否为空进行判断,因为key为空的对象都存储在table[0]中;若不为空,则按序对table[]和其存的链表进行顺序遍历。和put()方法步奏相同,先求出对象的hash值,然后计算其在数组中的位置,然后顺序遍历其对应链表,使用key.equals()方法或==进行key比较,找到所需的对象。

其他函数:
克隆函数:是浅拷贝

不可变对象的好处:
String, Interger这样的wrapper类作为HashMap的键是很合适的,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值