对于HashMap的理解

在看面经的时候,看到有前辈说到了面试时被提到HashMap的底层原理的问题。自己曾经也用过,对于它的理解仅仅停留在知道它是一个键值对,知道使用put get remove仅此而已。好像对它的底层从没关注过。好方。
对于一个类的使用,无非是从初始化开始了。常用的HashMap初始化时用到的构造方法,一个无参数,一个带参数。

public HashMap() {
        //当构造一个新的HashMap的时候,其实就是新建了一个类型为 HashMapEntry类型的数组
        table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
        threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
    }
        //ENMPTY_TABLE的大小为2
private static final Entry[] EMPTY_TABLE
            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];

从上面的源码可以看出,当new一个无参数的HashMap的时候,实际上是新建了一个大小为2的HashMapEntry型的数组。
再看带参数的构造方法

    public HashMap(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity: " + capacity);
        }

        if (capacity == 0) {
            @SuppressWarnings("unchecked")
            HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }

        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = Collections.roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }

若给定的capacity的值小于0则抛出异常,为0相当于无参的构造方法,大于最大值的将大小设置为给定的最大值。否则执行Collections.roundUpToPowerOfTwo(capacity);方法。此方法得出的结果总是一个2的n次方。在makeTable方法中,新建了个空间为计算好的HashMapEntry类型的数组,将其赋值给原数组。

接下来看一看HashMapEntry这个内部类,先贴源码

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

        HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override public final boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry<?, ?> e = (Entry<?, ?>) o;
            return Objects.equal(e.getKey(), key)
                    && Objects.equal(e.getValue(), value);
        }

        @Override public final int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
        }

        @Override public final String toString() {
            return key + "=" + value;
        }

HashMapEntry实现了Entry接口。有四个属性,key,value,hash, 以及HashMapEntry型的next。根据这些我们可以看出来HashMap实际上是由一个数组实现的。数组的每个元素是HashMapEntry型的,每个元素还有一个同类型的next指针。简单来说就是哈希表的拉链法。可以去翻阅数据结构中的哈希表。在HashMapEntry这个类中,除了get和set方法之外,还有三个方法,equals()和toString()不用过多介绍,来看看hashCode(),返回值为int类型,返回的是hash值。
HashMap中,最核心的两个方法无非是put和get。先看一看put方法

    @Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

首先如果put的值为null,则执行putValueForNullKey(value)。否则,根据key的值计算出hash值,然后根据hash值计算出数据对应存放于数组中的位置。遍历此数组中这个位置中的数据。如果之前此位置的数据链中存在相同的key,value的数据。那么将新的数据保存,并返回旧的数据。如果放入新数据后,大于threshold。就会进行扩容,执行doubleCapacity().然后计算扩容后新数据需要存放在新数组的位置。调用addNewEntry(key, value, hash, index)进行存放。下面看看addNewEntry方法

    /**
     * Creates a new entry for the given key, value, hash, and index and
     * inserts it into the hash table. This method is called by put
     * (and indirectly, putAll), and overridden by LinkedHashMap. The hash
     * must incorporate the secondary hash function.
     */
    void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
    }

如果,数组此位置原来没有数据,那么table[index]为空,直接在此位置存放数据。如果原来此位置有数据。则将原来此位置的第一个数据,挂到这个待存放新数据的后面。然后将新数据存放到数组中去。HashMapEntry的最后一个参数table[index],其本质就是HashMapEntry的下一个数据。
扩容的实际操作在doubleCapacity()中,看一下它的源码

    private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }

方法的返回值是一个HashMapEntry型的数组。先将原数组赋值给一个中间变量oldTable。如果原数组的大小已经是MAXIMUM_CAPACITY。那么不再扩容,直接返回原数组。否则将数组大小变为原来的两倍。如果原来数组中无数据存放,直接返回新的数组。不用进行数据的存放。如果原数组中有数据,那么要将原来数组的数据,存放到新的数组中去。
最后看一下get方法

    public V get(Object key) {
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

首先进行非空判断,然后根据key的值计算出hash值。然后根据hash值计算出在数组中的位置,进行key value的查找,如果相等, 则返回。如果不相等,则查找next。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值