HashMap底层源码分析(底部含给面试官讲HashMap话术)

底层数据结构剖析:

数组结构:存储区间连续、内存占用严重、空间复杂度大

优点:随机读取和修改效率较高,原因是数组是连续的(随机访问性强,查找速度快)。

缺点: 插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。

链表结构: 存储区间离散、占用内存宽松、空间复杂度小
优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活

缺点:不能随机查找,每次都是从第一个开始遍历(查询和修改效率低)

哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构。

可想而知,hashmap底层就是基于hash表的这样一种数据结构。

hashmap底层数据结构如下:

jdk1.7: 数组 + 链表

jdk1.8及以后:数组 + 链表/红黑树(链表节点>=8则转为红黑树,<=6则会编程链表)

类似于下面这种样子

image

hashmap源码解析

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

    // 默认初始容量,aka 16 表示1向左移4位,2的4次方
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // hashmap数组的最大容量,2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子,当集合的容量大于75%的时候,就进行扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 当链表长度大于8的时候,就调整成红黑树,(瞧瞧人家变量的起名多么的牛逼)
    static final int TREEIFY_THRESHOLD = 8;

    // 当链表长度小于6时,调整成链表
    static final int UNTREEIFY_THRESHOLD = 6;

    // 当链表长度大于8时,并且集合元素个数大于等于64,就转换为红黑树
    // 将链表转换为红黑树的数组长度最小值
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    // Node节点,对存入数据的封装
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // 存放的是key的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);
        }

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

        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;
        }
    }
    
    // 底层基于数组来进行元素的存放
    transient Node<K,V>[] table;

    
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    // 
    transient int modCount;

    // 用于存放每次扩容阈值
    int threshold;

    
    final float loadFactor;
    
    // 添加元素方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    // 计算hash值。
    static final int hash(Object key) {
        //没有直接使用hashcode()作为hash值,而是先将hashcode()值无符号右移16位得到新值,然后
		//hashcode()与这个新值进行异或计算得到最终的hash值(如果a、b两个值不相同,则异或结果为1。如果a、
		//b两个值相同,异或结果为0。)
        /**
        例如:
        * 00010101 00010100 01010100 01010001 原来计算的hash值
        * 00000000 00000000 00010101 00010100 无符号右移16位的新值
        * 00010101 00010100 01000001 01000101 上面两个值异或计算得到最终的hash值
        这样做的目的是避免hash碰撞
        */
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    /**
    onlyIfAbsent:false
    evict:true
    
    */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化时肯定为null
        if ((tab = table) == null || (n = tab.length) == 0)
            // 初始化时在这里调用resize()方法对数组进行初始化
            n = (tab = resize()).length;
        // 说明tab数组中某个位置是空的,没有数据
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 直接创建用于封装数据的node对象,进行tab[i]位置赋值即可。
            tab[i] = newNode(hash, key, value, null);
        // 说明tab数组中某个位置已经存在数据了,则这里进行链表构建
        else {
            Node<K,V> e; K k;
            // 进行hash比较,进行key比较,如果都相等,则说明要存放的键值对在数组中可能存在。
            // 直接进行替换就可以了
            // 为什么进行替换,是因为只能说明key相等,但是不一定能保证value也相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = 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) {
                        // 创建用于封装数据的node节点挂载到链表上去
                        p.next = newNode(hash, key, value, null);
                        // 判断链表长度是否大于等于8个节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 如果超过8个节点则转换成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
    
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 为空或者容量小于MIN_TREEIFY_CAPACITY(默认64)则不进行转换,而是进行resize扩容
        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 {
                // 循环遍历链表,切换为红黑树
                // 根据链表的node创建treenode
                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);
        }
    }

	TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
}

hashmap初始容量及扩容机制

public HashMap() {
    // 默认加载因子赋值
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

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或者不是个float类型的数字,抛异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    // 默认加载因子赋值
    this.loadFactor = loadFactor;
    // 计算出大于等于参数的第一个2的幂次方,重点!!!
    this.threshold = tableSizeFor(initialCapacity);
}

// 说白了以上初始化方法都没有对底层数组进行初始化。

// 直接传一个map集合进行初始化赋值
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

// 对底层数组进行初始化调用该方法
// 也就是你在调用put方法放元素的时候,或者是调用HashMap(Map<? extends K, ? extends V> m)构造方法时候,会对数组进行初始化
final Node<K,V>[] resize() {
    // 第一次放元素的时候,table数组为null
    Node<K,V>[] oldTab = table;
    // 一开始oldCap = 0
    int oldCap = (oldTab == null) ? 0 : oldTab.length; 
    // 一开始threshold默认是0,oldThr也就是0了
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 当数组容量达到最大值时,就不在扩容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
       // 将当前数组容量扩大一倍 oldCap << 1
       // 新的阈值也扩大一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 如果初始容量不为0,进行设置
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 如果你一开始啥也没干(就相当于是new HashMap())就会走到这里
        // newCap = 16
        newCap = DEFAULT_INITIAL_CAPACITY; 
        // newThr = 0.75 * 16 = 12
        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;
    if (oldTab != null) {
        // 循环遍历老map中的所有数据,迁移到新数组中对应位置,进行扩容操作
        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;
                        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;
                    }
                }
            }
        }
    }
    return newTab;
}

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

    if (loHead != null) {
        // 可见,当红黑树节点个数<=6的时候才会转换成链表
        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) {
        // 可见,当红黑树节点个数<=6的时候才会转换成链表
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

由此可见:初始化的 table.length 是多少?阀值(threshold)是多少?实际能容下多少元素?

默认情况下,table.length = 16,阀值threshold = 12,能存放 12 个元素,当存放第 13 个元素后进行扩容。

什么时候触发扩容?扩容之后的 table.length、阀值各是多少?

当 size > threshold 的时候进行扩容扩容之后的 table.length = 旧 table.length * 2,扩容之后的 threshold = 旧 threshold * 2

image

image

给面试官讲HashMap:

https://www.bilibili.com/video/BV1JA411e7Sj/?spm_id_from=333.337.search-card.all.click&vd_source=273847a809b909b44923e3af1a7ef0b1
HashMap几乎是我们日常开发中每天都会用到的一个集合,他是以键值对的形式进行存储。

在jdk1.7-jdk1.8之间,hashmap的实现略有区别。

其中两个重要的区别:一个是在jdk1.7及之前hashmap的底层数据结构采用的是数组加链表的方式。在jdk1.8以及之后采用的是数组加链表加红黑树的方式。红黑树的引用是为了提高它的查询效率。因为我们链表的查询效率是O(n),我们红黑树的查询效率是O(logn).

一个是在jdk1.7以及之前当我们遇到hash碰撞的时候,在链表上添加数据的时候采用的是头插法。但是到了jdk1.8以及之后采用的是尾插法。因为采用头插法会导致一些问题,比如在多线程的环境下,说会形成循环链表,进而耗尽我们cpu的性能。为了解决这个问题在jdk1.8以及之后采用的是尾插法。

在jdk1.7和jdk1.8之间还有一些其他的不同,这个我可能记得不太清楚了。比如说它的hash算法进行了一个简化。当然还有一些其他东西,我们需要在源码里面才能把他看的更透。但是源码我是读过的,但是有些东西我可能记得不太清了。

接下来我用jdk1.8和您聊一聊hashmap的一些基本原理。

按照阿里规约,我们在初始化hashmap的时候要指定初始容量。最好将这个初始容量指定成一个2的次幂的值(即使你传入的初始容量不是2的次幂,它底层也会帮你找到最近一个2的次幂的值,说白了就是不是你想传多少就是多少的)。当我们调用put方法的时候,它会使用key的hash值与上容量(也就是2的次幂-1的这个值)算出它在数组中的下标位置。(因为我们的容量都是2的次幂,2的次幂-1之后,它所有的低位都是1,高位都是0,和咱们的hash与之后,它一定能够与出一个下标在咱们容量之内的一个下标位置,因为与运算在计算机里面的效率非常高。所以他采取的是与运算而不是取余运算,取余运算的效率非常的慢)。然后查看数组当前位置有没有元素,如果没有元素的话,直接放上去就可以了,如果有元素的话,先通过key的hash值比较一下两个元素的是不是相同的,如果不同,则直接挂到链表尾部,如果相同,再比较键是否相同,如果相同的话,就进行value的替换,如果不同的话,就直接挂到链表的尾部。

当然在向hashmap集合中添加数据的时候会产生两个问题,一个问题是扩容的问题,一个问题是树化的问题。

关于扩容的问题,在hashmap里面有一个成员变量叫加载因子。当我们hashmap的size,也就是你插入元素的数量>=容量*加载因子的时候(也就是16X0.75)也是就是说当size大于12的时候,就会进行一次扩容(扩容是以2倍的方式扩容的),当然,当我们链表上面挂的节点元素足够多的时候,它也会进行树化,当然树化是一个很耗性能的操作,当然树化和扩容都是一个很耗费性能的操作。树化的前提是我们的链表长度一定要大于等于8,当然这还不够,当然在我们hashmap源码里面还有一个成员变量叫树化的最小容量,也就是我们数组的容量如果没有达到64,他会优先选择扩容数组,而不是对链表进行树化,也就是说树化是有两个条件构成的。

当然,阿里规约里面要求我们传入初始容量,其根本目的就是为了减少扩容。里面的计算公式是:你将来要存入的数据的数量/扩容因子-1。

关于hashmap我就和您聊这么多,您还有什么需要问我的吗?

Map集合的实现类

HashMap[重点]:

Jdk1.2版本,线程不安全,运行效率快;允许用null,作为key或是value。

Hashtable:

Jdk1.0版本,线程安全,运行效率慢;不允许null作为key或是value。基本上在实际项目开发过程中这个类是不用的。

Propertise:继承自HashTable

Hashtable的子类,要求key和value都是String。通常用于配置文件的读取。

TreeMap:

实现了SortedMap接口(是Map的子接口),可以对key自动排序。

TreeSet底层的实现原理使用的就是TreeMap

posted on 2021-01-30 18:16  ~码铃薯~  阅读(77)  评论(0编辑  收藏  举报

导航