详细解析jdk7HashMap源码

序:

       在java语言中,业务开发人员经常会使用map来做内存缓存,但是hashmap里面的坑是有很多的,比如内存溢出啦,死循环啦,丢数据啦……,是不是说的HashMap一无是处呢!也不是这样,它使用起来是很方便的。java源码系列

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

1.实现了Map接口,可以用k-v来存储数据。

2.实现了Serializable接口,可以序列化。

3.实现了Cloneable接口,支持克隆方法。

一 数据结构

HashMap采用了一种数组加链表的方式存储数据。

数组:

  查找和添加数据比较方便,效率高。

  删除数据需要移动数据,效率低。

链表:

 删除和添加只需要移动指针的指向,效率高。

查找需要整链表查找,效率低。

那HashMap的链表结构是怎样的呢?如下图:

HashMap模式数组大小是16,每个数组里面存的Entry里面有4个值(key,value,hash,next),装载因子是0.75  阈值大小是16*0.75 =12,也就是说第一个扩容是在第12个数据赋值的时候。

解释一下为什么HashMap采用了数组+链表的形式?

答:首先之前说过了,数组和链表各有优缺点,数组加链表的方式把他们的有点结合了,其次里面涉及了hash冲突,比如 哈希算法是%2运算,3%2 =1 ,5%2=1,他们得到的都是同一hash值,所以存的时候3的next指向5(5是跟在3的后面)。

hash原理


二 运行原理


 HashMap put方法判断逻辑运行图

步骤详解:

1.判断是否是空表,如果是空表,初始化这个表。

2 如果key是null,将null的这个key保存在table中。

2.hashmap中是否有这个key,如果有就用新的值替换掉旧的值,返回旧值。

3.判断是否超过阈值,如果超过就扩容,方式采用2的倍数来扩。为什么是2的倍数,源码里面有一个取数组下标的方法用的是hash&(length-1) 其中hash就是hash值,length就是数组的长度。hash&(length-1) 和 hash%length的 效果是一样的。&比%的性能更好,好多少?可以看这个博客。  只有是2的倍数才符合这个规律,所以扩容用了2的倍数。

4.添加新值。

三 源码注释

put方法的源码解析,上面的步骤只是说了一个大概,现在源码详细分析每一步。

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);  //若果是空表,还没有数值,就初始化这个
    }
    if (key == null)
        return putForNullKey(value);  //如果key是null,就将null添加进table
    int hash = hash(key);  //获取hashMap定义的一种hash方法
    int i = indexFor(hash, table.length);  //取数组的下标
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {   //将数组下面的链表循环,看有没有key相等,如果有,新值替换旧值。
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  //判断如果是存在key,新值替换,为什么用==判断了又用equals?想知道详细信息看这篇博客
            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) {   //初始化这个表
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);  //取2的n次方要》=初始化这个值 如果 toSize 为 12,13,14,15,16,capacity=16  若果toSize =17 那么capacity=32

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  //阈值是 capacity * loadFactor  默认capacity=16,loadFactor=0.75
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);   //初始化hash掩码,在hash函数里面会用到,知道是hash里面的就可以了
}
private V putForNullKey(V value) {   //把key为null的值放到表里面
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {  //如果只要找到有key为null的就直接替换,这就是说HashMap里面最多只能有一个key为null的键值对。
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);   //添加这个值,第一个参数是hash值,第二个参数是key,第三个参数是value,第四个参数是在表里面的位置,默认放在了第一个数组里面
    return null;
}
final int hash(Object k) {   //哈希函数
    int h = hashSeed;   //hashSeed 就是之前在initHashSeedAsNeeded方法里面设置的值

    if (0 != h && k instanceof String) {   //看到没,如果是String,用的还是不一样的哈希方法
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();  // 获取key的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);  //右移位 20,12,中间再异或,你可以看做特定的hash方法就可以了。
},
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);
}
void addEntry(int hash, K key, V value, int bucketIndex) {   //添加一个元素到链表中
    if ((size >= threshold) && (null != table[bucketIndex])) {  //大于等于阈值并且数组里面要有值的情况下,就进行扩容,以2的倍数
        resize(2 * table.length);  //扩容
        hash = (null != key) ? hash(key) : 0;  //因为之前key为null,添加元素也是调用了addEntry方法,所以这个对key进行了判null。有没有人会问,这个为什么又进行hash计算,因为数组扩容了。
        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];  //使用头插法插入数据,也就是说如果老表中存在hash值一样三个值a,b,c ,链的顺序是a->b->c,到新的表里面hash值也一样的话,数值的顺序变成了c->b->a
            newTable[i] = e;
            e = next;
        }
    }
}

转换的前后关系图。



至此,put方法就讲完了。

下面来讲讲get方法,流程就不说了,直接讲解源码

public V get(Object key) {  //获取值
    if (key == null)
        return getForNullKey();  //如果key为空,
    Entry<K,V> entry = getEntry(key);  //获取值

    return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {  
    if (size == 0) {  //表的长度为0 直接返回size,里面有些东西没有讲,size用的是volatile修饰,如果在获取的时候有一个线程把数据删除了,它是可以立马知道的,由于篇幅限制,这版就不讲了
        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))))  //一步步查找,找到key相等的就将值返回
            return e;
    }
    return null;
}

好了,hashmap的put和get方法到这里就全讲完了。

HashMap的使用时需要有几个注意的地方:

1.HashMap是不安全的,在扩容的时候回造成死循环和数据丢失。解决办法1.不用再多线程中使用,2 Collections.synchronizedMap(Map)来做同步限制,性能较差,可以用ConcurrentHashMap替代。

2.注意HashMap使用的时候最好对数据量做一个评估。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值