Java中hashMap的源码初探

大城市里の小女人 提交于 2020-03-01 23:58:41

最近一段时间准备沉下心来,认真学习底层的东西。今天从HashMap开始吧。看源码,我个人觉得应该带着问题去看,去学习大师们怎么做的。

{:toc}

  • HashMap 底层是用什么数据结构实现的?
  • HashMap 怎么扩容?
  • HashMap 怎么解决Key冲突?
  • HashMap 是线程安全的吗?
  • HashMap 是怎么把数据均匀的分布到容器的?

HashMap 底层是用什么数据结构实现的?

我们先来看看,HashMap实现类定义的变量:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final Entry<?,?>[] EMPTY_TABLE = {};
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    transient int size;
    int threshold;
    final float loadFactor;
    transient int modCount;
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

可以从上面代码中,看到定义了默认的初始容量,最大容量,默认的容量因子等等。而实现存储的是一个Entry数据,Entry对象的数据结构是什么样的?

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

        /*** Creates new entry.*/
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

可以看到这个对象四个属性,K,V,next(可以看作是一个指向下一个数据的指针),一个hash码。那当我们往Map里面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的时候,以Key做了一个Hash出来一个hash值,然后根据这个hash值在获取在entry数据中的位置,之后在检查是否有相同的Key,如果有相同的Key就覆盖掉原来的值,返回原来的值;如果没有的话,直接增加到entry数组中,返回空。

看到这里,我们应该可以明白HashMap怎么来存储了。

hashMap是以entry数组来存储的,而为了解决hash冲突,entry内部有一个next属性,指向下一个entry元素。也就是说,如果hash到冲突之后,就存入当前hash entry值的下一个,这样就把一个一个冲突的值链起来来了。

HashMap 怎么扩容?

从PUT方法里可以看出。如果hash出来的值经过indexFor 得出的数组下标,直接进行了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中,该下标已经存在了某个值,并且 当前数组的容量已经大于了设定了阈值了。这时候,就会进行扩容resize方法,而且传给resize的参数是2倍的当前容量。

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方法里面做了一些验证,然后把老数据转移(transfer)到了新的数据里面。

HashMap 怎么解决hash冲突?

我们从上面的Put方法我们可以看到,put的时候,检查了遍历了,每个entry的元素及冲突的链表。 如果冲突之后,会把当前值增加到相同的index的entry的后面形成 一个链表形式的冲突值列表。

HashMap 是线程安全的吗?

查看源码,我们没有发现任何与同步有关的方法,hashMap是非线程安全的。Hashtable是线程安全的,但是查看源码只是把所有的方法加了synchronized关键字,性能肯定差很多。

HashMap 是怎么把数据均匀的分布到容器的?

这个需要仔细的研究一下,put方法中的hash方法以及 indexFor 方法。

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

  /**
     * Returns index for hash code h.
     */
    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);
    }
    

从代码上面来看,key经过hash,然后有经过了一个&操作;注意上面的一个注释: // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

lenght必须设置成非零且是2的整数倍。为什么是2的整数倍呢?2的整数倍有什么好处?

大家可以看一下这个博客:讲的很清楚,实际上,当lenght为2的整数倍时,h&(length-1)和h%length 是等价的,但是效率上有很大的差别。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!