美文网首页Java
HashMap(1.7)源码解析

HashMap(1.7)源码解析

作者: 西安法律咨询服务平台与程序员 | 来源:发表于2020-04-13 12:31 被阅读0次

HashMap的数据结构是基于数组+链表实现的。

1 HashMap中重要的静态常量

  • DEFAULT_INITIAL_CAPACITY(默认初始化容量)
    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认初始化为1左移4位,为16。要求初始化容量必须为2的幂次方。这里的容量指的是HashMap中数组的长度,即桶的数量。
疑问:为什么HashMap的初始化容量必须为2的幂次方呢?

  • DEFAULT_LOAD_FACTOR(默认加载因子)
      /**
       * The load factor used when none specified in constructor.
       */
      static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    默认加载因子为0.75。
    疑问:加载因子作用是啥?

2 HashMap中的实例变量

  • table(存储键值对的数组,又称为桶数组)
    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
  • size(实际键值对数量)
        /**
       * The number of key-value mappings contained in this map.
       */
      transient int size;
    
  • loadFactor(加载因子)
        /**
       * The load factor for the hash table.
       *
       * @serial
       */
      final float loadFactor;
    
  • threshold(阈值)
        /**
       * The next size value at which to resize (capacity * load factor).
       * @serial
       */
      // If table == EMPTY_TABLE then this is the initial capacity at which the
      // table will be created when inflated.
      int threshold;
    
    HashMap扩容的阈值,到达该阈值后,HashMap可能进行扩容。
    threshold=capacity * load factor

3 HashMap存储键值对的内部节点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;
        }

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

Entry中存储的信息:

  • key
  • key所对应的hash
  • value
  • 该Entry的下一个节点

值得关注的是Entry的equals方法:
当且仅当两个Entry的key相同,value也相同时,两个Entry才equals。

4 HashMap常用默认构造方法

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

上面的无参构造方法使用调用下面的有参构造方法,其中初始容量为16(扩容阈值为16),加载因子为0.75;

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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();
    }

5 HashMap put方法详解

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    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;
    }
  • 如果table数组下标为i的桶内没有元素,则根据key和value创建一个entry,将其放入下标i的桶中,并返回null。
  • 如果talbe数组下标为i的桶内存在元素,并在存在相同key的元素,则使用新value替换该key所对应的旧值。
  • 如果talbe数组下标为i的桶内存在元素,并且存在相同key的元素则根据key和value创建一个entry,将其放入下标i的桶中,并返回null。


    image.png

5.1 inflateTable方法(初始化数组)

    /**
     * Inflates the table.
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

该方法主要作用:

  • 将HashMap的capacity(容量)调整为2的幂次方
  • 重新计算扩容threshold(阈值)
  • 根据capacity初始化Entry数组
    疑问:HashMap为啥需要将capacity调整为2的幂次方?

5.2 putForNullKey方法(存放key为null的Entry)

    /**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

我们一般将HashMap中数组table,index为0的位置称为第一个桶,index为1的位置称为第二个桶,以此类推。
HashMap中将key为null的键值对存储在第一个桶内。存放key为null的键值对存在两种情况:

  • 数组table的第一个桶,内有元素(不一定是key为null的元素,还有其他元素),循环从该桶的链表中查找key为null的键值对。如果找到,则替换key为null的键值对中value的值(即使用新value值替换key所在entry中的value),并返回旧value值;如果没有找到,则根据key和value新建一个entry对象,将其放在第一个桶内,并返回null。
  • 数组table的第一个桶,没有元素,根据key和value新建一个entry对象,将其放在第一个桶内,并返回null。

5.3 hash方法

    /**
     * Retrieve object hash code and applies a supplemental hash function to the
     * result hash, which defends against poor quality hash functions.  This is
     * critical because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    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);
    }

hash函数,计算key的哈希值

5.4 indexFor方法(计算数组的下标)

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

5.5 addEntry方法

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

如果HashMap中键值对的个数size超过阈值,并且key所在桶内不存在键值对,则将该HashMap扩容至2倍。

5.6 resize方法

    /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    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);
    }

该方法作用:

  • 创建新table数组
  • 调用transfer方法将旧table数组中的数据迁移至新table数组
  • 重新计算HashMap的扩容阈值threshold

5.7 transfer方法

    /**
     * Transfers all entries from current table to newTable.
     */
    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];
                newTable[i] = e;
                e = next;
            }
        }
    }

该方法将当前table数组中的元素移动到新table数组中:

  1. 首先判断是否需要对key,进行重新hash,如需要则重新计算散列值
  2. 根据hash值,重新计算数组的下标(即所在的桶)
  3. 将取出的元素插入到桶中链表的头部

6 HashMap Get方法

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

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

6.1 getForNullKey方法

    /**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    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;
    }

getForNullKey从table数组的第一个桶里面,循环查找key为null的entry所对应的value;若没有找到,则返回null

6.2 getEntry方法

    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    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;
    }

getEntry:

  1. 计算key的hash值
  2. 根据hash值计算key在table数组中所在的下标(即找到桶)
  3. 在桶中便利链表,若找到与该key值相等的entry,则返回entry内的value;否则返回null

7 疑问解答

  1. 为什么HashMap的容量必须为2的幂次方呢?
    因为计算key所在数组下标时,我们的目的是将key所对应的hash值映射到数组下标0---length-1内即可。当然方法有很多,比如取余,我们需要找一个高效的方法达到该目的。
    当容量为2的幂次方时,使用key所对应的hash值h与数组长度length进行”与“操作,比hash值h与数组长度length进行取余操作快。因为计算对位操作,更为迅速。
    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);
    }
  1. 加载因子作用是啥?
    加载因子loadFactor,用来影响HashMap的扩容时机。threshold=capacity * load factor,当HashMap中键值对数量size达到threshold阈值并且添加键值对中key所对应的桶内已存在其他键值对,则扩容。

相关文章

网友评论

    本文标题:HashMap(1.7)源码解析

    本文链接:https://www.haomeiwen.com/subject/heclmhtx.html