Java 深入理解 HashMap (底层数据结构、阈值、扩容、哈希冲突、JDK1.7与1.8中HashMap的区别)

深入理解HashMap

  1. jdk1.7和jdk1.8的HashMap的底层数据结构是什么?
  2. HashMap初始容量大小和加载因子分别是多少?
  3. 链表转红黑树的阈值是多少?
  4. 红黑树转链表的阈值是多少?
  5. HashMap的哈希函数怎么设计的?
  6. 为什么采用hashcode的高16位和低16位异或能降低hash碰撞?
  7. 两个键的hashcode相同,如何存储键值对?
  8. 有哪些办法解决hash冲突?HashMap是怎么解决hash冲突的?
  9. 在解决hash冲突的时候,为什么HashMap不直接用红黑树?而是先用链表再用红黑树?
  10. HashMap在什么条件下扩容?为什么扩容时2的次幂?
  11. HashMap的get过程?
  12. HashMap的put过程?
  13. HashMap1.8版本用的是头插法还是尾插法?
  14. HashMap1.8版本相对于1.7版本主要做了哪些改进?
  15. HashMap和HashTable有什么区别?
  16. HashMap、LinkedHashMap、TreeMap有什么区别?
  17. HashMap、TreeMap、LinkedHashMap各自的使用场景?
  18. 为什么HashMap是不安全的?如何规避HashMap的线程不安全?

1. jdk1.7和jdk1.8的HashMap的底层数据结构是什么?

  • jdk1.7底层数据结构: HashMap是数组、链表

  • jdk1.8底层数据结构: HashMap是数组、链表、红黑树

  • 数组的特点:查询效率高,插入删除效率低

  • 链表的特点:查询效率低,插入删除效率高

  • 在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入和删除效率都很高。引入红黑树解决过长链表效率低的问题。

HashMap1.7的底层数据结构;

在这里插入图片描述

HashMap1.8的底层数据结构;

在这里插入图片描述

2. HashMap初始容量大小和加载因子分别是多少?

  • 初始容量是16,加载因子是0.75。 ThreadLocalMap初始大小为16,加载因子为2/3.
  • 加载因子 是衡量哈希表密集程度的一个参数,如果加载因子越大,说明哈希表被装在的越多,出现hash冲突的可能性越大,繁殖,被装载的越少,出现hash冲突的可能性越小,如果过小,内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的平衡。

3. 链表转红黑树的阈值是多少?

在这里插入图片描述

class HashMap{
    ...
    
    //链表转红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    //红黑树转链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    
    
    ...
    
}

数组长度大于64且同的长度大于8。

关于 8 :

桶中的节点频率遵循泊松分布,从桶长度k的频率表可以看出,桶长度超过8的概率不到千万分之一

红黑树占用空间比较大,大概是常规链表的两倍,所以超过了8才选择改为红黑树。

关于 64 :

如果数组长度小于64,则会对数组扩容,而不是链表转为红黑树。只有两个条件都满足才会链表转为红黑树。

原因: 如果数组比较小,应该尽量避免红黑树结构。红黑树结构较为复杂,红黑树需要进行左旋、右旋、变色这些操作才能保持平衡。在数组容量较小的情况下,操作数组要比操作红黑树更加节省时间。

我们看到 putValue 方法

put 方法

 public V put(K key, V value) {
     //先计算key的hash值,然后再调用putVal
        return putVal(hash(key), key, value, false, true);
    }

putVal 方法

//onlyIfAbsent为false,说明如果已经存在相同(== 、equals)的key,则覆盖并返回旧值。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            //没有产生hash碰撞,即table的第i个同还没有元素,直接插入
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //此时第i个同已经存在元素,且p是这个桶的第一个元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //先比较第一个元素,如果hash值相等并且(是同一个key|| 两个key equals),直接跳到最后进行旧值覆盖
                e = p;
            else if (p instanceof TreeNode)
                //如果第i个桶是红黑树的话,执行红黑树的插入逻辑
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //如果第i个桶是一个链表,则遍历整个链表
                //利用binCount来计数链表的节点数
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //已经遍历到链表最后,则在尾部添加一个节点
                        p.next = newNode(hash, key, value, null);
                        //加入此时链表有8个节点,遍历到第8个节点的时候(此时binCount为7,binCount初始值为0)
                        //条件成立,则链表转变为红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //遍历的过程中,如果与其中一个节点的key 的hash值相等并且(同一个key || 两个key equals),直接跳到最后旧值覆盖
                    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;
    }

链表转红黑树的 treeifyBin 方法

/**
 * 将链表节点转为红黑树节点
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
    	//1. 如果table为空,或者 table的长度小于64,调用热size方法进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
    	//2. 根据hash值计算索引值,将该索引值位置的节点赋给e,从e开始遍历该索引位置的链表
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                //3.将链表节点转红黑树节点
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //4.如果是第一次遍历,将头结点赋值给hd
                if (tl == null)//tl为空代表为第一次循环
                    hd = p;
                else {
                    //5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
                    p.prev = tl;
                    tl.next = p;
                }
                //6.将p节点赋值给tl。用于在下一次循环中作为上一个节点进行一些链表的关键操作(p.prev = tl 和 tl.next = p)
                tl = p;
            } while ((e = e.next) != null);
            //7.将table该索引位置赋值给新转的TreeNode的头结点,如果该节点不为空,则以以头结点hd为根节点,构建红黑树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

4. 红黑树转链表的阈值是多少?

见 3 图:红黑树节点数小于6,红黑树转成链表。

HashMap 红黑树转链表的 split 方法:

resize() --> split()
 /** 这个方法在HashMap进行扩容时会调用到:  ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  * @param map 代表要扩容的HashMap
  * @param tab 代表新创建的数组,用来存放旧数组迁移的数据
  * @param index 代表旧数组的索引
  * @param bit 代表旧数组的长度,需要配合使用来做按位与运算
  */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
     		//做个赋值,因为这里是((TreeNode<K,V>)e)这个对象调用split()方法,所以this就是指(TreeNode<K,V>)e对象,所以才能类型对应赋值
            TreeNode<K,V> b = this;
            //设置低位首节点和低位尾节点
            TreeNode<K,V> loHead = null, loTail = null;
            //设置高位首节点和高位尾节点
    		TreeNode<K,V> hiHead = null, hiTail = null;
            //定义两个变量lc和hc,初始值为0,后面比较要用,他们的大小决定了红黑树是否要转回链表
    		int lc = 0, hc = 0;
    		//这个for循环就是对从e节点开始对整个红黑树做遍历
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                //取e的下一节点赋值给next遍历
                next = (TreeNode<K,V>)e.next;
                //取好e的下一节点后,把它赋值为空,方便GC回收
                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;
                }
            }

    		//如果低位链表首节点不为null,说明有这个链表存在
            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);
                }
            }
    		//如果高位链表首节点不为null,说明有这个链表存在
            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);
                }
            }
        }

5. HashMap的哈希函数怎么设计的?

Hash函数设计: hash 函数是先拿到 key 的 hashcode, 它是一个 32 为的 int 值,然后让 hashcode 的高16位于低16位进行异或操作。

hash函数作用: HashMap 采用 hash 算法来决定每个元素的存储位置(真正找到数组中下标则是通过寻址算法 (n-1) & hash 这也是取模运算的变体)

  //Java8的散列值优化函数,也叫扰动函数
  static final int hash(Object key) {
        int h;
        //右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,
        //就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

6. 为什么采用hashcode的高16位和低16位异或能降低hash碰撞?

  • 32位哈希码的空间范围大,[-2^31 ~ 2^31-1],前后加起来有42亿多的映射空间,哈希码右移16位(32bit的一半),高半区和低半区做异或,混合了原始哈希码的高位和低位,加大了低位的随机性,减少哈希碰撞
  • 混合后的低位掺杂了高位的部分特征,高位的信息也被变相保留了下来。

详细见 https://deep-sea-tramp.blog.csdn.net/article/details/111243211

7. 两个键的hashcode相同,如何存储键值对?

hashcode相同,通过 equals 比较内容是否相同。若相同,则新的 value 覆盖以前的 value。 若不相同,则将新的键值对存储到 HashMap

8. 有哪些办法解决hash冲突?HashMap是怎么解决hash冲突的?

8.1 发生hash碰撞的条件

**HashMap 中哈希碰撞(冲突)的条件是指不同key被映射到同一个桶。**当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了。

8.2 解决 hash 冲突常用方法

  • 链地址法:

    所有哈希地址为 i 的元素构成一个成为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因为查找、插入、删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。HashMap中使用的就是链地址法

在这里插入图片描述

  • 建立公共溢出区:

    将哈希表分为公共表和溢出表,发生溢出时,将溢出数据存入溢出区。

  • 开放地址法:

    从发生冲突的单元起,按照一定的顺序从哈希表中找出一个空白单元,然后把冲突元素存入该单元的方法;所需长度>=元素个数;开放地址中解决冲突的方法:线性探测法(ThreadLocal),平方探测法,双散列函数探测法

    示例:

    • 假设关键字集合为 { 12 , 33 , 4 , 5 , 15 , 25 },表长为 10。我们用散列函数 f(key) = key mod 10 计算地址。key = 15时,发现 f(15) = 5,与 5 所在的位置冲突。我们应用上面的公式 f(15) = (f(15) + 1) mod 10 = 6 将 15 存入下标为 6 的位置

    • 在这里插入图片描述

  • 再哈希:

    同时构造多个不同的哈希函数,第一个哈希函数冲突,使用第二个,以此类推。

9. 在解决hash冲突的时候,为什么HashMap不直接用红黑树?而是先用链表再用红黑树?

  • HashMap 解决 hash 冲突的时候,先用链表,再转红黑树,是为了时间和空间的平衡。
  • TreeNodes 占用的空间大小大约是普通 Nodes 的两倍,只有在容器中包含足够的节点保证使用才用它,在节点数比较小的时候,对于红黑树来说,内存上的劣势会超过查找等操作的优势,使用链表更加好。
  • 节点数比较多的时候,综合考虑时间和空间,红黑树比链表要好。

HashMap 与红黑树

红黑树需要进行左旋右旋变色等操作来保持平衡,而单链表则不需要。当元素小于8个的时候,做查询操作,链表结构已经能保证查询性能。

当元素大于 8 个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

对 HashMap 的相应位置进行查询的时候,就回去循环遍历这个超级大的链表,性能不好。 java8使用红黑树来代替 8 个节点数的链表后,查询方式性能得到了很好的提升,从原来的 O(n) 到了 O(log n)

10. HashMap在什么条件下扩容?为什么扩容是2的次幂?

10.1 HashMap的扩容条件以及扩容

扩容会发生在两种情况下(满足任意一种条件就可以发生扩容):

  1. 当前存入的数据个数大于扩容阈值(例如 16* 0.75)即发生扩容
  2. 存入数据到某一条链表上,此时长度大于 8,且数组长度小于64 即发生扩容

扩容:每次扩容的容量都是之前容量的2倍。 HashMap 的容量是有上限的,必须小于 1 << 30 即 2^30。如果容量超出了这个数,则不再增长,且阈值会被设置为 Integer.MAX_VALUE。 HashMap 1.8版本的扩容相对于1.7版本,性能方面做了优化(见 14 问),1.8版本的HashMap扩容后,节点在新数组的位置只有两种,原下标位置或者原下标+旧数组的长度

相关源码:put()->putVal() treeifyBin() 其中有对是否要resize()做判断

10.2 为什么扩容是2的次幂

HashMap 的初始容量是 2 的 n 次幂,扩容也是 2 倍的形式进行扩容,可以使得添加的元素均匀分布在 HashMap 中的数组上,减少 hash 碰撞,充分利用内存空间(所有下标都能用上)。

HashMap的长度为什么是 2 的幂次方具体数学原因:

看到寻址算法:https://deep-sea-tramp.blog.csdn.net/article/details/111243057

  1. 公式:( n - 1 ) & hash
  2. 当 n 为 2 的指数次幂时,减 1 后换算成 2 进制,则每一位都为 1 , 与 hash 进行与运算, 就可以得到 ( 0 ~ n-1 )范围内的每一个 index
  3. 当 n 不为 2 的指数次幂时,减 1 后换算成 2 进制,二进制数中会出现 0,与 hash 进行与运算,会导致 ( 0 ~ n-1 ) 范围内的某些 index 永远得不到

寻址算法 (n-1) & hash 当 n为 2 的次幂的时候,等同于 hash % n,其中n为数组长度,我们也知道在计算机中除法和取模运算是性能低下的,而位运算则能提高运算效率。此外,如果 n 是奇数,如 15 即1111 减 1 之后再和 hashcode进行与运算,最后一位就是 0 ,有一些位置就不能存放元素,极大浪费空间。

11. HashMap的get过程?

  1. 通过 hash 值获取 key 映射到的桶
  2. 根据该桶的存储结构决定是遍历红黑树还是遍历链表
  3. table[i] 的首个元素是否和 key 一样,如果相同则返回该value
  4. 如果不同,先判断首元素是否是红黑树节点,如果是,则去红黑树中遍历查找,反之去链表中遍历查找。

相关源码:

public V get(Object key)

 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)

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    	//如果当前table没有数据的话返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //根据当前传入的hash值以及参数key获取一个节点即为first,如果匹配的话返回对应的value值
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {//如果参数与first的值不匹配的话
                //判断是否是红黑树,如果是红黑树的话先判断first是否还有父节点,然后从根节点循环查询是否有对应的值
                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过程图:

在这里插入图片描述

12. HashMap的put过程?

  1. 对key的hashCode()做hash运算,计算index
  2. 查看 table[index] 是否存在数据,没有,则构造一个Node节点存放在其中;
  3. 存在数据,说明发生了 hash 冲突,继续判断 key 是否相等,相等,用新的 value 替换原数据;
  4. 若不相等,判断当前节点类型是不是树形节点,如果是树形节点,创造树形节点插入红黑树中;(如果当前节点是树形节点证明当前已经是红黑树了)
  5. 若不为树形节点,创建普通 Node 加入链表中;判断链表长度是否大于 8 并且数组长度大于 64,则链表转换为红黑树
  6. 插入判断当前节点数是否大于阈值,如果大于,则扩容为原数组的两倍

相关源码: put()->putVal() treeifyBin()

put流程图:

在这里插入图片描述

13. HashMap1.8版本用的是头插法还是尾插法?

头插法与尾插法:

在这里插入图片描述

HashMap1.7版本用的是头插法,多线程场景下,1.7版本的头插法存在死循环的风险,1.8版本用的是尾插法,改进了存在死循环的缺陷。HashMap1.7在并发情况下,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%占用问题,所以一定要避免在并发环境下使用HashMap。

HashMap1.8版本在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的应用关系。并发环境下,优先考虑使用ConcurrentHashMap

1.7版本为什么会出现死循环?头插法导致死循环示例:
在这里插入图片描述

以下死循环情况部分参考博客:https://blog.csdn.net/thqtzq/article/details/90485663

jdk1.7中,扩容的核心源码如下:

 void resize(int newCapacity) {//传入新的容量
        Entry[] oldTable = table;//引用扩容前的Entry数组
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
            threshold = Integer.MAX_VALUE;//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
            return;
        }
        Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组
         //将数据转移到新的Entry数组里
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //HashMap的table属性引用新的Entry数组
        table = newTable;
        //修改阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
            	//1, 获取旧表的下一个元素
                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;
            }
        }
    }
  1. 假设旧表的初始长度为 2,此时已经在下标为 1 的位置存放了两个元素,再 put 第三个元素的时候需要考虑扩容

    在这里插入图片描述

  2. 此时两个线程AB都进行 put 操作,线程 A 先扩容,代码执行到Entry<K,V> next = e.next,线程A挂起;

    然后线程B开始执行transfer函数中的while循环,会把原来的table变成一个table(线程B自己的栈中),再写入到内存中。

    在这里插入图片描述

    注意,因为线程A的e指向了 key(3),next指向了key(7),其在线程B rehash后,指向了线程B重组后的链表。我们可以看到链表的顺序被翻转了。

  3. 线程A被唤醒,继续执行:

    • 先执行了 newTable[i] = e;

    • 然后是 e = next,导致了 e 指向了 key(7)

    • 而下一次循环的 next=e.next 导致 next指向了 key(3);

      如下图:

      在这里插入图片描述

  4. 当前循环:

    e.next = newTable[i];
    newTable[i] = e ;
    e = next;
    将key(7)摘下来采用头插法,放到newTable[i]的第一个元素中,下一个结点指向key(3)
    下一次循环:
    next = e.next; 此时e为key(3)
    此时next = null; 不会在往下循环了。

    在这里插入图片描述

  5. 此时key(3)采用头插法又放到newTable[i]的位置,导致key(3)指向key(7),注意此时key(7).next已经指向了key(3),所以环形链表就出现了。如下图:

    在这里插入图片描述

    于是当我们的线程A调用get()方法时,如果下标映射到3处,则会出现死循环。

总结:
线程A先执行,执行完Entry<K,V> next = e.next;这行代码后挂起,然后线程B完整的执行完整个扩容流程,接着线程A唤醒,继续之前的往下执行,当while循环执行3次后会形成环形链表

1.8版本尾插法源码分析:(在putVal()方法中)

				...

 				//如果第i个桶是一个链表,则遍历整个链表
                for (int binCount = 0; ; ++binCount) {  //利用binCount来计数链表的节点数
                    if ((e = p.next) == null) {
                        //已经遍历到链表最后,则在尾部添加一个节点
                        p.next = newNode(hash, key, value, null);
                        //假如此时链表有8个结点,遍历到第8个结点的时候(此时binCount为7,binCount初始值为0),
                        //条件成立,则链表转变为红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }    
    
				...

14. HashMap1.8版本相对于1.7版本主要做了哪些改进?

  1. 数据结构的差异 1.7 数组+链表 ;1.8 数组+链表或红黑树
  2. 链表的插入方式优化 1.7头插法; 1.8尾插法
  3. 扩容的改进 1.7全部rehash ;1.8简单判断(判断新增位是0还是1),要么原位置,要么【原位置+旧容量】的位置
  4. 插入数据的差别 1.7先判断是否要扩容再插入; 1.8先插入,插入完成再判断是否需要扩容

1.7版本中的插入:(1.8为put和putVal方法)

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

为什么1.8不选择也是先扩容再加?(复制知乎上别人的回答)

jdk7先扩容,然后使用头插法,直接把要插入的Entry插入到扩容后数组中,头插法不需要遍历扩容后的数组或者链表。而jdk8如果要先扩容,由于是尾插法,扩容之后还要再遍历一遍,找到尾部的位置,然后插入到尾部。(也没怎么节约性能)

感觉jdk8可能浪费性能的地方,在Node插入之后,如果当前数组位置上节点数量达到了8,先树化,然后再计算需不需要扩容,前面的树化可能被浪费了。

15. HashMap和HashTable有什么区别?

  1. HashMap是线程不安全的,HashTable是线程安全的;
  2. 由于线程安全,所以HashTable的效率比不上HashMap;
  3. HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable则不允许;
  4. HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时,扩大两倍(1.7与1.8又不一样),后者扩大两倍+1
  5. 添加键值对时的 hash 值算法不同:HashMap 的 hash 算法是扰动函数,而 HashTable直接使用对象的 hashCode ;

HashTable初始容量为11

public Hashtable(){
    this(11,0.75f);
}

HashTable扩容为2倍+1

 protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // 扩容为2倍+1
        int newCapacity = (oldCapacity << 1) + 1;
        
     	...
    }

HashTable直接使用对象的hashCode作为hash

 public synchronized V put(K key, V value) {
       
     	...
            
        Entry<?,?> tab[] = table;
     	//直接使用对象的hashCode作为hash
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;

     	...

        addEntry(hash, key, value, index);
        return null;
    }

HashMap 的扩容:要么在【原位置】要么在【旧位置+旧容量】,通过判断新增位是 0 还是 1,如果是 0 就在【原位置】,如果是 1 就在【旧位置+旧容量】。

						//resize()方法中对链表的扩容判断处理
						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //oldCap 1 0000
                            //e.hash 1 0110 -> 【旧位置+旧容量】
                            //e.hash 0 0110 -> 【原位置】
                            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;
                        }

16. HashMap、LinkedHashMap、TreeMap有什么区别?

共同点: 线程不安全

不同点:数据无序、数据有序、数据有序还可以对数据进行排序

  1. HashMap 中 key 的值没有顺序,相对于 LinkedHashMap 和 TreeMap,使用更广泛。
  2. LinkedHashMap 内部有一个双向链表(head、tail、双向),保持Key插入的顺序。访问顺序和插入顺序是一致的。
  3. TreeMap 的顺序是 key 的自然顺序(如整数从小到大),也可以指定比较函数。访问顺序和插入顺序不一定是一致的。

三者的使用方法:

public class MapsTest {
    public static void main(String[] args) {
        testHashMap();
        System.out.println("===========");
        testLinkedHashMap();
        System.out.println("===========");
        testTreeMap();
    }
    private static void testHashMap() {
        Map<String, String> hashMap = new HashMap<String, String>();
        hashMap.put("name1", "图图11");
        hashMap.put("name2", "图图12");
        hashMap.put("name3", "图图13");
        Set<Map.Entry<String, String>> set = hashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator = set.iterator();
        while(iterator.hasNext()) {
            Map.Entry entry = iterator.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("HashMap ,  key:" + key + ",value:" + value);
        }
    }

    private static void testLinkedHashMap() {
        Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
        linkedHashMap.put("name1", "图图21");
        linkedHashMap.put("name2", "图图22");
        linkedHashMap.put("name3", "图图23");
        System.out.println("开始时顺序:");
        Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator = set.iterator();
        while(iterator.hasNext()) {
            Map.Entry entry = iterator.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("LinkedHashMap, key:" + key + ",value:" + value);
        }
        System.out.println("通过get方法,导致key为name1对应的Entry到表尾");
        linkedHashMap.get("name1");
        Set<Map.Entry<String, String>> set2 = linkedHashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator2 = set2.iterator();
        while(iterator2.hasNext()) {
            Map.Entry entry = iterator2.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("LinkedHashMap, key:" + key + ",value:" + value);
        }
    }

     static void testTreeMap() {
        TreeMap<String,String> map = new TreeMap<String,String>(new xbComparator());
        map.put("name1", "图图31");
        map.put("name2", "图图32");
        map.put("name3", "图图33");
        Set<String> keys = map.keySet();
        Iterator<String> iter = keys.iterator();
        while(iter.hasNext())
        {
            String key = iter.next();
            System.out.println("TreeMap , key is:  "+key+" ,value: "+map.get(key));
        }
    }
    static class xbComparator implements Comparator
    {
        public int compare(Object o1,Object o2)
        {
            String i1=(String)o1;
            String i2=(String)o2;
            return i1.compareTo(i2);
        }
    }
}

运行结果:

HashMap ,  key:name3,value:图图13
HashMap ,  key:name2,value:图图12
HashMap ,  key:name1,value:图图11
===========
开始时顺序:
LinkedHashMap, key:name1,value:图图21
LinkedHashMap, key:name2,value:图图22
LinkedHashMap, key:name3,value:图图23
通过get方法,导致key为name1对应的Entry到表尾
LinkedHashMap, key:name2,value:图图22
LinkedHashMap, key:name3,value:图图23
LinkedHashMap, key:name1,value:图图21
===========
TreeMap , key is:  name1 ,value: 图图31
TreeMap , key is:  name2 ,value: 图图32
TreeMap , key is:  name3 ,value: 图图33

规律:

  1. HashMap无序
  2. LinkedHashMap 插入和get都会往队尾放入(最新的都在队尾)
  3. TreeMap 有序 由小到大 compareto()返回-1为小

17. HashMap、TreeMap、LinkedHashMap各自的使用场景?

见16的顺序特性

18. 为什么HashMap是不安全的?如何规避HashMap的线程不安全?

  • HashMap 不安全的原因:内部没有锁的机制,多个线程某个时刻同时操作HashMap并执行put操作,且hash值相同,这个时候就需要解决冲突。很多方法如put()、addEntry()、resize()等都是不同步的。
  • 并发场景下,优先考虑使用ConcurrentHashMap替换HashMap,不建议使用HashTable替换HashMap,因为ConcurrentHashMap是HashTable的优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值