jdk源码分析之HashMap

HashMap的底层数据结构

HashMap底层采用数组加链表的数据结构存储键值对
Hash根据key的哈希值转化为数组的下标将键值对存入数组中,数组的元素是一个链表,冲突的key放置在数组的同一个位置,使用链表将冲突的数据链接起来
数组的底层结构如下:

     /**
     * An empty table instance to share when the table is not inflated.
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

可见数组存储的元素是Entry,而Entry表示链表的节点,节点存储了key、value,key的hash值,指向下一个节点的指针域next
Entry数据hashCode()方法是通过key的hash值和value的hash值异或得到的
equals方法判断两个Entry相等的标准是key相等且value相等
重写equals方法必须重写hashCode方法,满足两个对象相等(equals返回true)则这两个对象的hash值必须相等

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

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

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

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

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

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

数组的长度是2的整次幂,h & (length-1)等价于

h % length

但是求模运算通过除法实现,除法的的运算比较耗费CPU时间,因此巧妙地转化为位运算来求模

根据key获取entry

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

首先判断size是否为0,size为0表示HashMap中没有键值对,此时通过key获取value必然返回null
然后判断key是否为null,null作为key,其hash值为0,null key在entry数组中的位置为index=0处
key不为null的话通过indexFor(hash, table.length)计算桶的位置
根据桶的位置获取链表头,然后遍历链表获取所求的entry

Entry<K,V> e = table[indexFor(hash, table.length)];

此时的e表示链表的头部,此链表的所有节点key的hash值相同(冲突),但是key不同,因此扫描链表,获取对应key的entry,然后返回此entry。

键key为null时候的特殊处理

HashMap允许null的键和null的值,键为null的entry都放在entry数组索引为0地方,即下标为0的桶内(也有可能其他非null键hash值刚好为0,因此需要扫描比较)

    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;
    }
    如果size==0,则value必然为null,直接返回null
    否则通过Entry<K,V> e = table[0]获取链表头部
    通过e.key == null找到key为null的entry,返回此entry的value

通过键获取值

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

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

分类处理,key为null通过getForNullKey()获取对应的value
否则通过getEntry(key)获取对应的entry,通过entry获取value

向entry链表添加一个新的entry

 /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    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);
    }

首先判断size >= threshold,如果存储的键值对数量size大于阈值threshold的话,需要扩容,然后rehash,重新计算hash值和桶的位置
然后调用createEntry把entry添加到链表头

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

根据key删除数据

根据key的hash值找到桶,遍历桶中链表,找到对应key的entry,删除。删除的办法就是entry的前序节点的后继指针直接指向entry节点的后继节点

    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

如果size==0,HashMap中没有保存任何键值对,对任何key取value均返回null
否则:
计算key的hash值,key==null设置key的hash值为0
否则通过hash(key)计算hash值
然后根据key的hash值计算桶的位置

int i = indexFor(hash, table.length);

然后开始遍历链表,找到与传入key相等(==或equals)的entry
此entry的前序节点的后继指针指向entry的后继节点,跳过此entry节点即可

判断是否包含某个value

    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

HashMap判断是否包含某个value,只能通过穷举的办法,在每个桶内一个一个挨着比较,找到就返回true
HashMap接受null的键值,null的键存储在下标为0的桶内,null的值存储在对应key所在的桶内
判断是否包含某一特定的value,需要分类讨论
如果该value是null,则不能通过equals方法比较,通过==比较

    /**
     * Special-case code for containsValue with null argument
     */
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

如果value不为null,穷举查找比较

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;

猜你喜欢

转载自blog.csdn.net/shihui512/article/details/51493327