HashMap 源码分析

2017-04-15  本文已影响0人  喝口苏打水

HashMap涉及到的东西还真是挺多的.看了几篇文章也算是个总结吧

前置知识

在分析HashMap之前先说一些基础的知识.

HashCode

它是一个int类型的值由jdk根据对象计算得出.每个对象都会有这个值.这里涉及到和equals方法的关系.在Java中规定,如果equals()方法返回true,那么比较的hashCode必须是相等的.因此重写equals方法必须要重写hashCode方法.

Map的存储结构

Map的内部有一个Entry类,,它是一个单链表,存放了key,value,以及下一个Entry.
Entry存储在一个数组上,一个位置放一个单链表.

Map的存储方式
通过key获取vlaue的过程,首先通过key的hash值获取到map数组存放的索引,然后再通过key的equals方法获取对应的值.

map的加载因子
默认为0.75 在hashMap中实际存储量是数组的size * 0.75 也就是size为16实际存储12以上元素的时候就会把数组尺寸扩大一倍为32.如果加载因子为1,那么数组上都会有链表.查询会非常耗时.如果数值偏小,那么会浪费空间.
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

源码分析

构造函数
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();
    }

并没有实际创建 一个Map,数组也没有创建.真正的创建是在put方法里调用的inflateTable()

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();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

传入的对象获取hash值,然后进行异或运算.
h ^= (h >>> 20) ^ (h >>> 12);
h ^ (h >>> 7) ^ (h >>> 4);
为什么要经过这样的运算呢?这就是HashMap的高明之处。先看个例子,一个十进制数32768(二进制1000 0000 0000 0000),经过上述公式运算之后的结果是35080(二进制1000 1001 0000 1000)。看出来了吗?或许这样还看不出什么,再举个数字61440(二进制1111 0000 0000 0000),运算结果是65263(二进制1111 1110 1110 1111),现在应该很明显了,它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分布。

那这样有什么意义呢?
如果通过key获取value值,需要经过key的hash值来获取到map存放数组的索引,h & (length-1),这样得到的结果就是一个比length小的正数,我们把这个值叫做index。其实这个index就是索引将要插入的值在数组中的位置.因此上面操作希望得到的索引更加的均匀.这样每个数组上的都尽量平均覆盖到单链表.

h&(length-1)为什么会比length小?
当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。

put方法

在看put源码之前需hash方法作为支撑.现在再来看一下put方法

public V put(K key, V value) {
       if (table == EMPTY_TABLE) {
         //如果数组为空,则把数组扩容,扩容后的长度为2的n次方
           inflateTable(threshold);
       }
      //如果key为null,那么存放的位置是数组索引为0的位置
       if (key == null)
           return putForNullKey(value);
       //获取hash值
       int hash = hash(key);
       //获取索引
       int i = indexFor(hash, table.length);
      //遍历单链表,如果key的equals返回true则覆盖老的oldValue
       for (Entry<K,V> e = table[i]; e != null; e = e.next) {
           Object k;
       //如果重写了equals方法,不重写hashCode此时就会有问题了.
           if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
               V oldValue = e.value;
               e.value = value;
               e.recordAccess(this);
               return oldValue;
           }
       }
     //修改了map的结构
       modCount++;
     //新增一个Entry
       addEntry(hash, key, value, i);
       return null;
   }
addEntry方法

这个方法主要是判断新增的Entry类是否会导致Map的尺寸.如果超过就将数组的长度扩展为当前的两倍.

void addEntry(int hash, K key, V value, int bucketIndex) {
//size代表在这个map中包含键值对映射数量.
//如果新增Entry类size是否大于阀值(默认是12),指定索引的数组是不为空,
        if ((size >= threshold) && (null != table[bucketIndex])) {
//map尺寸增加当前table长度两倍
//阀值是32*0.75
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

createEntry方法

新建一个Entry在单链表的头部.next指向下一个Entry对象

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
      //新的Entry指向当前存放的链表的头部
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
resize方法

Map当中设置map的最大尺寸
static final int MAXIMUM_CAPACITY = 1 << 30; 2的三十次方.
当指定的超过这个数时也Map的size也不会增加了.
此时阀值会变成int最大值,也就是2的三十一次方.
这部分我很疑惑,既然达到了2的三十次方不会再次增加了,那么阀值也就没有存在的意义了吧.何必还要符合规则是当前的两倍呢.设置为MAXIMUM_CAPACITY +1即可

 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];
        //原来的Entry放到新的数组中,所以hash值要重新计算.
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

以上基本是一个put的过程.取也类似.

上一篇 下一篇

猜你喜欢

热点阅读