HashMap源码分析(JDK 8)

目录

基本组成结构

类的继承关系

基本成员属性

构造函数

核心方法

put

resize

get

remove

entrySet

HashMap常见面试题


基本组成结构

HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现,Value 随意。jdk 8 之前,其内部是由数组+单向链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树。

大致的数据存储形式如下:


类的继承关系

/**
 * HashMap继承了抽象父类AbstractMap
 * 实现了Map(定义了一组通用的操作)
 * 实现了Cloneable(可进行浅拷贝)
 * 实现了Serializable(可序列化)
 */
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 

基本成员属性

/**
 * 底层存储容器
 * Node类型的数组,每个Node元素都是一个链表的头结点,通过它可以访问连接在其后面的所有结点
 */
transient Node<K,V>[] table;

/**
 * table的默认容量:16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * table的最大容量
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认负载因子,用于扩容
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表转红黑树的阈值
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树转链表的阈值
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 最小转红黑树的容量
 * (就是说,即使链表长度达到转红黑树的阈值,但总容量没有达到该值,也不会转红黑树)
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 键值总数
 */
transient int size;

/**
 * 用于迭代过程中防止结构性破坏的标量
 */
transient int modCount;

/**
 * 下一次扩容的阈值 threshold = capacity * load factor
 */
int threshold;

/**
 * 负载因子
 */
final float loadFactor;



构造函数

    /**
     * 自定义初始化容量与负载因子
     */
    public HashMap(int initialCapacity, float loadFactor) {
        // 初始化容量不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 初始化容量大于最大默认容量则取最大默认容量为初始化容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 负载因子不能小于等于0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // tableSizeFor返回大于等于initialCapacity的最小2的指数幂
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * 自定义初始化容量
     */
    public HashMap(int initialCapacity) {
        // 负载因子取默认值0.75f
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 默认方式
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 将m中的所有元素添加至HashMap中
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

核心方法

put

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 计算key的hash地址
     * 为了使hash值尽可能分散,将key的hash值的高32位与低32位进行异或运算,得到新的hash值
     */
    static final int hash(Object key) {
        int h;
        // 如果key为null,默认在table[0]的位置,由此看出key是允许为null的
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 1. 如果table还未被初始化,那么初始化它
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 2. 如果为null,说明此索引位置并没有被占用,(n - 1) & hash:得到数组下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else { // 3. 不为null,说明此处已经被占用,只需要将构建一个结点插入到这个链表的尾部即可
            Node<K,V> e; K k;
            
            // 3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同,
            // 且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 3.2 如果p这个头结点是红黑树结点的话,在红黑树中查找
            // 存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {// 3.3 如果不是前两种情况,遍历此链表,将构建一个结点插入到该链表的尾部
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果插入后链表长度大于等于 8 
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 在遍历过程中,若发现与某个结点的key值相等,这依然是一次修改操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            // 如果e不是null,说明当前的put操作是一次修改操作,且e指向的就是需要被修改的结点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;

        // 4. 如果添加后,数组容量达到阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     * table太小,改成扩容操作
     */
    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);
        }
    }

put(K key, V value)方法总结:

1. 先判断table是否初始化,如果没有,则进行初始化。可见table真正初始化是在第一次put的时候。

2. 通过key的hash值计算出新增结点在数组中的位置。如果该位置为null,直接将其存入该位置。

前面通过hash()方法将key的hash值的高32位与低32位进行异或运算,得到新的hash值。这么做的目的是为了使hash值尽可能分散。

然后通过(n - 1) & hash结果得到数组下标,n为table的当前容量,下标范围应该是0~(n-1)范围内的整数。

以n=16为例,(n - 1) & hash可看作:1111 & 01001101010011010100110101001101 

由此可以看出,为了让结果覆盖最多的可能,n-1的二进制值应该都为1,因此n的取值应该是2的指数幂,这也是table容量的取值为什么一定是2的指数倍的原因。

3. 如果该位置不为null,说明此处已经被占用,此时需要判断是覆盖还是插入。

3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同,且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值。

3.2 如果该位置的头结点是红黑树结点的话,在红黑树中查找,存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null。

3.3 如果不是前两种情况,遍历此链表。在遍历过程中,若发现新结点与某个结点的key值相等,覆盖该结点,否则将新结点插入到该链表的尾部。如果插入后链表长度大于等于 8 ,执行treeifyBin(tab, hash)方法。

3.4 在treeifyBin(tab, hash)方法中需要注意,并不是直接将链表裂变成红黑树,而是先判断table是不是小于64,如果是则会进行扩容操作,只有table足够大了(>=64),才会转红黑树。

4. 添加完新结点后,如果数组容量达到阈值,进行扩容操作。


resize

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 旧数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // oldCap > 0 说明数组已经初始化完成,此处需要给旧数组扩容
        if (oldCap > 0) {

            // 如果容量达到极限将不再扩容,直接返回旧table
            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时
           说明使用了构造函数 HashMap(int initialCapacity, float loadFactor)初始化
           根据传入的容量initialCapacity计算出一个合适的容量暂存在阈值中 */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        // 数组未初始化并且阈值也为0,一切都以默认值进行构造
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * 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;

        // 旧数组为null则初始化,不为null则进行扩容
        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)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    // 将链表中的各个结点原序地转移至新表中
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;

                            // 判断e在扩容后的索引是否变化
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 链表被一分为二,一部分在原位置,一部分在新位置
                        if (loTail != null) {
                            loTail.next = null;
                            // 原位置
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // 新位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        // 无论是扩容还是初始化,都返回 newTab
        return newTab;
    }

resize()方法总结:

  1. 判断是初始化调用还是扩容调用,计算新的数组容量与下一次扩容的阈值,同时创建出新数组。
  2. 如果是初始化调用,直接返回新数组;如果是旧数组扩容,还需要将旧数组中的各个结点复制到新数组中,其中包括单个结点,链表结点与红黑树结点的复制。

旧结点在新数组中地址分配过程分析:

单个结点:根据 e.hash & (newCap - 1) 计算出在新数组中的位置。

链表结点:由于扩容后,容量会左移一位,因此可以根据 e.hash & oldCap 来判断扩容后链表元素的 hash 值参与计算的部分是否有变化,无变化的部分在原位置 newTab[index],有变化的部分在扩容后的新半区 newTab[index + oldCap]。

e.hash & oldCap 是如何以判断 hash 值是否变化的?

以oldCap = 16 为例:

index = e.hash & (oldCap - 1) = 010010......1001 & 1111 = 1001

无变化情况: e.hash & oldCap = 010010......01001 & 10000 = 0

有变化情况: e.hash & oldCap = 010010......11001 & 10000 = 1

即通过判断hash值新加入比较的一位是0还是1。

红黑树结点:与链表方式类似,同样将树分裂出一部分到newTab[index + oldCap],如果分裂后的树结点过少,则以链表形式重组。


get

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

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 当table不为null时,且key的hash值所在坐标上不为null时
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {

            // 判断头结点是否匹配
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 如果是红黑树,则遍历红黑树查找匹配结点
                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);
            }
        }
        return null;
    }

get(Object key)方法总结:

  1. 当数组不为空时,计算传入参数key的hash值,定位其在数组中的位置。
  2. 如果该位置不为null,先通过 == 或 equals方法比较key与该位置头结点的key是否匹配,匹配则返回结点的value值。
  3. 如果是链表或红黑树,则通过遍历的方式查找匹配的结点,找到后返回结点的value值。
  4. 如果以上情况都没有匹配的结点,直接返回null。

remove

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    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不为null时,且key的hash值所在坐标上不为null时
        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;
            // 查找要删除的结点,找到后用node指向该结点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // node为要删除的结点,再判断是否要匹配value
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 如果node是红黑树结点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 如果node是头结点,直接将node.next设为头结点
                else if (node == p)
                    tab[index] = node.next;
                // 如果node是中间结点,node的前结点p的next结点指向node的next结点
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

remove(Object key)方法总结:

  1. remove方法就是查找 + 删除的过程,首先要根据key值找到要删除的结点,过程与get方法一致。
  2. 找到要删除的node结点后,如果是红黑树结点,直接调用红黑树的删除方法;如果是一个头结点,那么用node.next 结点代替它作为头节点存放在 table[index] 中;如果是链表的中间结点,使node的前一结点的next直接指向node.next 结点即可。
  3. 成功删除node后,remove方法会返回node.value值;如果未找到要删除的结点,直接返回null。

entrySet

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    /**
     * HashMap的内部类
     * 实现了Iterable接口,因此可用来迭代
     */
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

        // foreach遍历EntrySet的时候时间上是会遍历table[]
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                // 通过modCount禁止多线程并发写操作
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

entrySet()方法总结:

  • entrySet返回了EntrySet对象,EntrySet是HashMap的内部类,实现了Iterable接口,它可以直接操作HashMap底层的数组table,因此可以对HashMap的键值对集合进行迭代。
  • keySet、values与entrySet作用相似,keySet返回了HashMap的key的集合,values返回value的集合。
  • 另外,由于HashMap是非线程安全的,因此在所有迭代操作过程中,都有modCount变量来限制并发下的结构性破坏操作。

HashMap常见面试题

1. HashMap的原理,内部数据结构?

底层使用哈希表(数组 + 链表),当链表过长会将链表转成红黑树以实现O(logn)时间复杂度内查找。

2. 讲一下HashMap中put方法的过程。

i. 对Key求Hash值,然后再计算下标;

ii. 如果没有碰撞,直接放入桶中;

iii. 如果碰撞了,以链表的方式链接到后面;

iv. 如果链表长度超过阈值(TREEIFY_THRESHOLD == 8),并且总容量超过64,就把链表转成红黑树;

v. 如果节点已经存在就替换旧值;

vi. 如果桶满了(容量 * 负载因子),就需要resize。

3. HashMap中hash函数是怎么实现的?还有哪些hash的实现方式?

i. 高16bit不变,低16bit和高16bit做一个异或运算;

ii. (n - 1)& hash 得到下标;

4. HashMap怎样解决冲突,讲一下扩容过程,加入一个值在原数组中,现在移动了新数组,位置肯定改变了,那是什么定位到在这个值新数组中的位置?

i. 将新节点追加到链表上;

ii. 容量扩充为原来的二倍,然后对每个节点重新计算哈希值;

iii. 这个值只可能在两个地方,一个是在原下标的位置,另一种情况时在下标为<原下标 + 原容量>的位置。

5. 抛开HashMap,hash冲突有哪些解决办法?

i. 链地址法(HashMap使用了该方式);

ii. 再哈希法(产生冲突时计算另一个hash函数地址,直到没有冲突为止);

iii. 开放定址法。

6. 针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),如何优化?

链表转为红黑树,JDK 8已经实现。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页