HashMap源码初尝及应用

240 阅读25分钟

思维导图

思维导图是个好东西,哈哈

继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

可以看出具有map的通性,也能够被克隆

底层结构

hashMap的hash算法

JDK1.7中

static int hash(int h) {
    // 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);
}

扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算

JDk1.8中

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);////key.hashCode()为哈希算法,返回初始哈希值
    }

扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算

key.hashCode()是Key自带的hashCode()方法,返回一个int类型的散列值。

32位带符号的int表值范围从-2147483648到2147483648,大概40亿的样子。

这么大的值里,一般是很难发生碰撞的,但是内存放不下这么长的数组,同时HashMap的初始容量只有16,

所以这样的散列值使用需要对数组的长度取模运算,得到余数才是索引值。

int index = (n - 1) & hash;

为什么HashMap的数组长度是2的整数幂?

以初始长度为16为举例,16-1 = 15,15的二进制数位是0000 0000 0000 0000 0000 1111,

再比如31的二进制是0000 0000 0000 0000 0001 1111,

可以看出一个基数二进制最后一位必然位1,当与一个hash值进行与运算时,最后一位可能是0也可能是1。

但偶数与一个hash值进行与运算最后一位必然为0,造成有些位置永远映射不上值。

相与下来高位全部为0,只保留低位,

但是这时,又出现了一个问题,即使散列函数很松散,但只取最后几位碰撞也会很严重。

这时候hash算法的价值就体现出来了,

扰动函数

hashCode右移16位,正好是32bit的一半,

与自己本身做异或操作(相同为0,不同为1),

也是为了混合哈希值的高位和低位,增加低位的随机性,

同时混合后的值也变相保持了高位的特征。

JDK1.7的底层结构

数组+链表

JDK1.8的底层结构

数组+链表+红黑树

先是数组加链表结构,当同一个hash值存放超过8个元素时,即当链表长度超过阈值(8)时,转换为红黑树结构,加快查询速度。

初始化

一些基本属性table、entrySet、size、modCount、threshold、loadFactor

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     * 表格,在第一次使用时初始化,调整大小为必要的。当分配时,长度总是2的幂。(在某些操作中,我们也允许长度为零目前不需要的引导机制。)
     * 其定义为 Node<K,V>[],即用来存储 key-value 的节点对象。在 HashMap 中它有个专业的叫法 buckets ,中文叫作桶。
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     * 保存缓存entrySet ()。注意,这里使用了AbstractMap字段表示keySet()和values()。
     * 
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     * 此映射中包含的键-值映射的数目
     * 容器中实际存放的node大小
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     * 这个HashMap在结构上被修改的次数,结构修改是指改变映射的数量HashMap或修改其内部结构(例如,重复)。
     * 记录容器被修改的次数
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     * 下一个需要扩容的阈值
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

    /**
     * The load factor for the hash table.
     * 负载因子 默认是0.75,综合考虑了时间和空间利用率
     * @serial
     */
    final float loadFactor;

内部桶的源码

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     * 基本哈希bin节点,用于大多数条目。(见下文TreeNode子类,在LinkedHashMap中为它的Entry子类。)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

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

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);//重写hashcode方法
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //重写equals方法
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

无参构造方法

这是用得最多的构造方法

/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     * 使用默认的初始容量构造一个空的HashMap,默认大小16,默认负载因子为0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

指定大小的构造方法

默认负载因子是0.75

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative. 当初始化大小为负数时,抛出异常
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

指定大小和负载因子的构造方法

这种情况下需要算出阈值。

如果操出了map的最大值,即2的30次方,就将值设置为最大值,

如果负载因子为负或者不是数字类型,都抛出异常。

/**
     * 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;
        this.threshold = tableSizeFor(initialCapacity);
    }

调用tableSizeFor方法获取大于该初始值的2的n次方值。

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;// >>> 代表无符号右移
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

前面所有的过程,都是保证造成一个所有位都位1的数据。并且通过最后的+1。变成2N次方格式的数据。

小结

一般在确定元素个数的情况下,还是使用传入初始值的构造方法。

常见api

put

源码解读

1.7的put方法

public V put(K key, V value) { 
    if (table == EMPTY_TABLE) { //如果哈希表没有初始化(table为空) 
        inflateTable(threshold); //用构造时的阈值(其实就是初始容量)扩展table 
    } 
    //如果key==null,就将value加到table[0]的位置 
    //该位置永远只有一个value,新传进来的value会覆盖旧的value 
    if (key == null)  
        return putForNullKey(value); 
   
    int hash = hash(key); //根据键值计算hash值 
   
    int i = indexFor(hash, table.length); //搜索指定hash在table中的索引 
   
    //循环遍历Entry数组,若该key对应的键值对已经存在,则用新的value取代旧的value 
    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; //并返回旧的value 
        } 
    } 
   
    modCount++; 
    //如果在table[i]中没找到对应的key,那么就直接在该位置的链表中添加此Entry 
    addEntry(hash, key, value, i); 
    return null; 
}    

addEntry 源码,涉及扩容

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 键值对数量超过阈值 并且 当前元素要存放的位置不为空
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); // 扩容(注意这里的扩容值和1.8的不一样)
        hash = (null != key) ? hash(key) : 0; // 重新计算哈希值
        bucketIndex = indexFor(hash, table.length); // 重新计算下标
    }
    // 加入新结点
    createEntry(hash, key, value, bucketIndex);
}

流程图:

1.8的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.
     * 使 key 和 value 产生关联,但如果有相同的 key 则新的会替换掉旧的
     *
     * @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) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, dont change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    // evict: true <=> 插入结点后是否允许操作(给子类LinkedHashMap用的)
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //当table为空的时候会进行扩容,插入第一个值的时候发生
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //计算下标,看是否有冲突,即是否存在值
        if ((p = tab[i = (n - 1) & hash]) == null)
        //没有冲突直接插入
            tab[i] = newNode(hash, key, value, null);
        //有冲突    
        else {
            Node<K,V> e; K k;
            // 如果头结点是要找的结点(key的哈希值并且key的值相同),e指向p
            if (p.hash == hash &&//hash值相同
                ((k = p.key) == key || (key != null && key.equals(k))))//key值相同
                e = p;
            //如果p属于树,调用树的插入方法  
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //如果是链表,循环查找到尾节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//找到尾节点
                        p.next = newNode(hash, key, value, null);//尾插法插入
                        // 判断是否超出树的阈值(8),是则树化,但是也要看是否超过了数组是否超过64,否则也只是执行扩容操作,具体看treeifyBin方法
                        // -1是因为除去了数组内的头结点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //进行树化
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 对链表中的相同 hash 值且 key 相同的进一步作检查
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 找到目标结点,覆写(如果允许的话 或 旧值为null)并返回旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;// 操作次数++
        //判断扩容条件与1.7相比判断条件少了null != table[bucketIndex]
        if (++size > threshold)
            //扩容
            resize();
        //用处不大(与LinkedHashMap有关)    
        afterNodeInsertion(evict);
        return null;
    }

treeifyBin 方法源码

    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     * 替换为给定散列的bin at索引中的所有链接节点,除非表格太小,这种情况下会调整大小。
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //判断table长度是否小于64,如果是,先进行扩容。
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //否则才进行树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

MIN_TREEIFY_CAPACITY 参数,可被树化的最小表容量

/**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     * 容器可能被treeified的最小表容量。(否则,如果容器中有太多节点,将调整表的大小。)
     * 至少为4 * TREEIFY_THRESHOLD,以避免冲突调整大小和treeification阈值之间。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

流程图:

put方法的变化

主要是1.8减少了resize的判定条件,增加了当同一个桶元素到达8个时,链表转化为树,8由头插法变成的尾插法,避免了循环。

get

源码解读

jdk1.7的get

public V get(Object key) {
    // key为空,调用getForNullKey方法,因为空键固定放在0号位
    if (key == null)
        return getForNullKey();
    // 得到key所在的结点
    Entry<K,V> entry = getEntry(key);
    // 结点为空直接返回null,反之获取对应val
    return null == entry ? null : entry.getValue();
}
public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

getEntry 方法

final Entry<K,V> getEntry(Object key) {
    // 空map返回null
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key); // 得到key的哈希值
    // 计算key对应下标i,并遍历i号位的链表,找到key值对应的结点
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        // 找到键为key的结点,判断条件:
        if (e.hash == hash && // 1.key的哈希值相同
                ((k = e.key) == key || (key != null && key.equals(k)))) // 2.key的值相同
            return e;
    }
    // 找不到就返回null
    return null;
}

jdk1.8的get

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

getNode方法,获取元素的关键

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; // 数组
    Node<K,V> first, e; // 结点
    int n;
    K k;
    if ((tab = table) != null // 数组不为空
            && (n = tab.length) > 0 // 数组长度大于0
            && (first = tab[(n - 1) & hash]) != null) { // 计算下标i,i号位不为空
        if (first.hash == hash && // always check first node 首先检查头结点是否是目标结点,是就返回
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果头结点后连得第一个是树节点,调用树的get方法
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 如果不是,那就是链表结构,那就遍历寻找
            do {
                // 找到了就返回
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 找不到就返回null
    return null;
}

get方法的变化

8相对7增加了一个树的查询操作

remove方法

/**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @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 remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

removeNode方法

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //当table不为空,并且hash对应的桶不为空时
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //桶中的头节点就是我们要删除的节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //用node记录要删除的头节点
                node = p;
            //头节点不是要删除的节点,并且头节点之后还有节点
            else if ((e = p.next) != null) {
            	//头节点为树节点,则进入树查找要删除的节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                //头节点为链表节点
                else {
                	//遍历链表
                    do {
                    	//hash值相等,并且key地址相等或者equals
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                             //node记录要删除的节点
                            node = e;
                            break;
                        }
                        //p保存当前遍历到的节点
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //我们要找的节点不为空
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                	//在树中删除节点
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //我们要删除的是头节点
                else if (node == p)
                    tab[index] = node.next;
                //不是头节点,将当前节点指向删除节点的下一个节点
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
    
    

removeTreeNode 方法

 /**
         * Removes the given node, that must be present before this call.
         * This is messier than typical red-black deletion code because we
         * cannot swap the contents of an interior node with a leaf
         * successor that is pinned by "next" pointers that are accessible
         * independently during traversal. So instead we swap the tree
         * linkages. If the current tree appears to have too few nodes,
         * the bin is converted back to a plain bin. (The test triggers
         * somewhere between 2 and 6 nodes, depending on tree structure).
         * 删除在此调用之前必须存在的给定节点。这比典型的红黑删除代码更混乱,因为我们不能与叶子交换内部节点的内容
         * 被可访问的“next”指针固定的后继者遍历期间独立完成。所以我们换了树联系。如果当前树看起来节点太少,
         * 箱子被转换回普通的箱子。(测试触发在2到6个节点之间,取决于树的结构)。
         */
        final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
                tab[index] = first = succ;
            else
                pred.next = succ;
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            if (root.parent != null)
                root = root.root();
            if (root == null
                || (movable
                    && (root.right == null
                        || (rl = root.left) == null
                        || rl.left == null))) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
                if ((p.right = sr) != null)
                    sr.parent = p;
                if ((s.left = pl) != null)
                    pl.parent = s;
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }

            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);//平衡二叉树方法

            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
        }****

扩容

jdk1.7的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.
 * 把当前map中的元素翻新到新的更大的新map中。当前map中的键值对达到阈值就会触发扩容方法。
 *
 * 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.
 * 如果当前容量已经达到最大容量,map将不会进行扩容,而是将阈值提到Integer.MAX_VALUE,从而达到以后不会再调用扩容方法的效果
 *
 * @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).
 *        新的容量,必须是2的幂次,必须比当前容量大,除非当前容量已经达到最大容量
 */
void resize(int newCapacity) {
    Entry[] oldTable = table; // 当前数组
    int oldCapacity = oldTable.length; // 当前容量
    // 已经达到最大容量的情况下,将阈值升到Integer.MAX_VALUE,能够有效阻止日后调用扩容
    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); // 重新计算阈值
}

transfer方法

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; // 指针记录e.next
            // 如果需要,重新计算哈希值
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); // 重新计算下标
            // 头插法
            e.next = newTable[i]; // 将e插入到table[i]头部之前
            newTable[i] = e; // 将链表下移
            e = next; // 指向下一个要移动的结点
        }
    }
}

jdk1.8的resize

涉及两个关键参数,即两个关键阈值

链表转为数结构阈值

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     * 对象使用树而不是列表时的bin计数阈值本。当向a添加一个元素时,箱子被转换为树至少有这么多节点的bin。
     * 这个值必须更大大于2,且至少为8,以符合假设删除关于转换回普通箱子的树收缩。
     */
    static final int TREEIFY_THRESHOLD = 8;

树转为链表结构阈值

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     * 存储计数阈值,用于在存储期间取消(分割)存储调整操作。应该小于TREEIFY_THRESHOLD,在大多数6到网格与收缩检测下去除。
     */
    static final int UNTREEIFY_THRESHOLD = 6;

resize方法

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 * 初始化或加倍表的大小。如果为空,则在中分配符合现场阈值的初始容量目标。
 * 否则,因为我们使用的是2的乘方展开
 * 每个容器中的元素必须保持在相同的索引中,或者移动新表中偏移量为2的幂次。
 * 初始化或翻倍数组。如果为空,初始化数组。
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 暂存旧table数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 暂存旧容量,如果是初始化调用扩容方法,旧容量为空,赋0
    int oldThr = threshold; // 暂存旧阈值
    int newCap, newThr = 0;
    // 旧容量不为0
    if (oldCap > 0) {
        // 旧容量达到最大容量,不再扩容,将阈值提高至Integer.MAX_VALUE,有效阻止以后再度出现达到阈值的情况
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 左移翻倍容量和阈值(前提是不超过最大容量并且旧容量需要超出默认容量
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold //两倍旧阈值
    }
    // 如果旧容量为0,即table数组还未创建
    // 为带参构造方法使用HashMap(int initialCapacity)及HashMap(int initialCapacity, float loadFactor)
    // 因为初始化容量一开始是被存放在threshold中的
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 旧容量为0,即table数组还未创建
    // 为无参构造方法使用HashMap(),使用默认值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 初始化阈值
    }
    // 如果新阈值为0
    // 一种情况是使用了带参构造方法(else if (oldThr > 0))
    // 另一种是旧容量未达到默认容量大小或翻倍后超出最大容量(else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    //                        oldCap >= DEFAULT_INITIAL_CAPACITY))
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor; // 计算新阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE); // 新容量满足要求
    }
    // 赋阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
            // 创建新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 赋给全局变量
    // 这里开始实施转移,transfer
    if (oldTab != null) {
        // 第一层,数组结点遍历
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 数组结点
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 释放旧数组
                if (e.next == null) // 尾结点,直接转移
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) // 树节点,调用树的split方法
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 链表结点,保留之前的顺序(尾插法)
                else { // preserve order
                    // 第二层,链表遍历
                    // (我们以该链表原本位置在010(2),旧容量为100(4)举例子,分析已知原本的哈希值可能为010,可能为110

                    // head记录头结点,tail记录尾结点
                    Node<K,V> loHead = null, loTail = null; // lo表示low表示0,记录要转移的结点
                    Node<K,V> hiHead = null, hiTail = null; // hi表示high表示1,记录保留在原位置的结点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 高位为0(010
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { // 高位为1(110
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 高位为0的链表,保持原位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 高位为1的链表,转移阵地
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回新数组
    return newTab;
}

小结JDK1.7和JDK1.8的resize方法的不同点

1.扩容条件不同 7是,达到阈值并且新增结点发生碰撞才扩容,也就是说,如果达到阈值后,新增节点插入位置为空,则先不扩容。

8是只要达到阈值就扩容,条件减少了。

2.扩容方式不同(针对链表而言)

7是采取和插入时一致的方式,头插法,再下移。

8是拆分链表后直接移动头结点。

3.扩容下标计算方式不同

7是重新计算。

8是根据高位二进制决定,为0则下标不变,为1则往后移动一个旧数组长度的距离。

一些常见问题

什么时候由链表转化为红黑树?

在上面普通方法源码中已经指出来,当同一个entry的数量达到8的时候会调用树化的方法treeifyBin,

但是在treeifyBin方法中,有个限制条件是table数组的长度需要达到64(MIN_TREEIFY_CAPACITY),最小树化容量,

否则就只是扩容,而不会树化。

需要达到最小树化容量的原因,为什么不达到8就直接树化?

1.首先链表的查询时间复杂度是O(n),树的查询时间复杂度是O(logn);

2.由于树形节点的大小大约是常规节点的两倍,所以我们只有当箱子包含足够的节点时才使用它们;

3.属性上规定是4*TREEIFY_THRESHOLD,而默认初始化,大小是16。

4.hashCode算法下所有bin中节点的分布频率会遵循泊松分布,而相同节点下的链表长度达到8的概率为0.00000006,

相当小,从而转换为红黑树的概率也小。

5.在链表数量少的情况下

即当链表长度为6时 查询的平均长度为 n/2=3

      红黑树为 log(6)=2.68时 :  链表  8/2=4

                   红黑树   log(8)=3

可见数量少的情况下,链表的查询速度更快,当超过8,就会是树的查询树洞更快了。

所以table稍等时候可以通过扩容,减少链表的长度,没必要转换为又占空间,又耗时的红黑树。

什么时候由红黑树转化为链表?

分为两种情况

1.在删除元素时

在删除元素时调用removeTreeNode方法中有段源码

if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }

此处并没有利用到网上所说的,当节点数小于UNTREEIFY_THRESHOLD时才转换,而是通过红黑树根节点及其子节点是否为空来判断

是否需要转换为链表。

在扩容时resize方法中

对红黑树进行了拆分

else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

红黑树拆分方法中

树的数量,如果数量小于6,就进行树转为链

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
            //分为两棵树,分别判断树的数量,如果数量小于6,就进行树转为链
            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

untreeify 解除树方法

        /**
         * Returns a list of non-TreeNodes replacing those linked from this node.
         *返回一个非树节点列表,替换链接的树节点此节点.
         */
        final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

为什么是6?

由于为6的时候,链表的查询树洞已经优于树了,同事链表也会更节省空间。

在操作put时也会少去树插入数据的过程。

为什么初始化容器大小是16?

1.首要原因是hash下标的算法,规定容量最好是2的n次方。

static int indexFor(int h, int length) {
    return h & (length-1);
}

因为这时候,h&(length-1)(与)才等价于h%length(模)。

2.length的值为偶数,length - 1 为奇数,则二进制位的最后以为为1,

这样保证了h & (length - 1)的二进制数最后一位可能为1,也可能为0。

如果length为奇数,那么就会浪费一半的空间。

3.至于为什么是16,应该是一个经验值,过大过小都不太好。

太小了就有可能频繁发生扩容,影响效率,太大了又浪费空间。

hashmap的负载因子是多大?为什么是这个值?

默认负载因子是0.75,

综合了时间利用率和空间利用率考虑后,选取的这个值。

如果过大比如为1,会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率

如果过小比如为0.5,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,

那么底层的链表长度或者是红黑树的高度就会降低,查询效率就会增加;

但是空间就会增加两倍,牺牲了空间来换取时间。

hashMap使用了哪些方法避免hash冲突?

1.使用链地址法(使用散列表)来链接拥有相同hash值的数据;

2.使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;

3.引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

为什么HashMap中String、Integer这样的包装类适合作为K?

1.String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率;

2.都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况;

3.内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况。

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,

而HashMap的容量范围是在16(初始化默认值)~2^30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,

从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;

在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度-1)来获取数组下标的方式进行存储,

这样一来是比取余操作更加有效率,

二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,

三来解决了“哈希值与数组大小范围不匹配”的问题。

HashMap 的长度为什么是2的幂次方?

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。

这个实现就是把数据存到哪个链表/红黑树中的算法。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现,

但是,重点来了:取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作,

也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方,

并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

1.8源码中进行了两次扰动处理,1次位运算 + 1次异或运算。

那为什么是两次扰动呢?

这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,

最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

写在后面的话

孤独是常态,你需要在学会在孤独中守住内心的美好。

参考

hashmap解析与1.7和1.8 put方法流程图与常见问题

基于jdk1.7和1.8的HashMap源码分析(侧重于哈希算法、put、get、resize)

HashMap详解以及源码分析

HashMap实现原理及源码分析

深入理解HashMap(五)remove方法解析

Java集合容器面试题(2020最新版

文章推荐

Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么?