HashMap常见问题

1.HashMap与HashTable

相同点

HashMap和HashMap都是基于哈希表实现的,其内部的每个元素都是key-value键值对,HashMap和HashTable都实现了Map、Cloneable、Serializable接口

不同点

  • 父类不同:HashMap继承了AbstracMap类,而HashTable继承了Dictinart类
    HashMap常见问题_第1张图片

  • 空值不同:HashMap允许空的key和value值,HashTable不允许空的key和value
    HashMap常见问题_第2张图片

  • 线程安全性:HashMap线程不安全,HashTable安全

  • 性能:虽然都是基于单链表,但是由于HashTable的put、get、remove操作加了synchronized锁,所以效率低在这里插入图片描述

  • 初始容量:

    HashTable初始长度11,每次扩容变为2n+1
    HashMap初始容量16,每次扩充变为原来的两倍
    创建给定初始容量时,HahsTable会直接使用给定大小,HashMap扩充到2的幂次方

2.HashMap与HashSet

  • HashSet集成了AbstractSet接口,实现了Set、cloneable、Serializable接口
  • HashSet不允许出现重复的值
  • HashSet底层就是HashMap,所有对HashSet的操作其实就是对HashMap的操作,不保证顺序
    HashMap常见问题_第3张图片

3.HashMap结构图

HashMap常见问题_第4张图片

4.AbstractMap类

  • Map接口的骨干实现
  • 集成这个类并且提供entrySet方法即可返回不可修改的map
  • 为了实现可修改,必须额外重写put()方法,并且entrySet.iterator必须实现remove()方法

5.Node接口

Node节点是用来存储HashMap的一个个实例,它实现了Map.Entry接口,我们先来看一下Map中的内部接口Entry接口的定义

6.Map.Entry

一个map的entry链,是这个Map.entrySet()返回的一个集合视图,包含了各种的元素
这个唯一的方式是从集合的视图进行迭代,获取一个map的entry链,这个Map.Entry链只在迭代期间有效
Node节点会存储四个属性,hash值,key,value,指向下一个Node节点的引用
因为Map.Entry是一条条entry链连接在一起的,所以Node节点也是一条条entry链

7.KeySet内部类

继承于AbstractSet抽象类,它是由HashMap中的keyset()方法来创建KeySet实例的,旨在对HashMap中的key键进行操作

    /**
   *  返回一个set视图,这个视图中包含了map中的key
   **/
    public Set<K> keySet() {
        // keySet 指向的是 AbstractMap 中的 keyset
        Set<K> ks = keySet;
        // 如果 ks 为空,就创建一个 KeySet 对象
        if (ks == null) {
            // 并对 ks 赋值
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

8.values内部类

和keyset类似,只是针对key-value键值对中的value值进行使用

9.EntrySet内部类

对key-value键值对进行操作的内部类

10.HashMap1.7底层结构

JDK1.7中,HashMap采用位桶+链表的实现,即使用链表来处理冲突,同一hash值的链表都存储在一个数组中。但是当未育一个桶中的元素较多,即hash值相等的元素较多时,通过key值以此查找的效率较低
HashMap大致结构
HashMap底层数据就是一个Entry数组,Entry是HashMap的基本组成单元,每个Entry中包含一个key-value键值对,每个Entry包含 【hash,key,value】 属性,如图:
HashMap常见问题_第5张图片

11.HahsMap1.8底层结构

与JDK1.7相比,1.8在底层结构方便做了一些改变,当每个桶中元素大于8的时候,会转变为红黑树,目的就是优化查询效率,JDK1.8重写了resize()方法
HashMap常见问题_第6张图片

12.HashMap重要属性

初始容量

HashMap的默认容量为: 16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

HashMap常见问题_第7张图片

最大容量

HashMap最大容量为: 2^30

static final int MAXIMUM_CAPACITY = 1 << 30;

Q?int占用4个字节,按说最大容量应该是左移31位
A:数值计算中,最高位也就是最左边的位数为符号位,代表正负,0->正数,1->负数

默认负载因子

HashMap默认负载因子为:0.75f

static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容机制的原则是当HashMap中存储的数量>HashMap容量*负载因子时,就会把HashMap的容量扩大为原来的两倍。

HashMap的第一次扩容就是在 16*0.75=12 时进行

树化阈值

HashMap的树化阈值为:8

static final float DEFAULT_LOAD_FACTOR = 0.75f;

在添加元素时,当一个桶中存储元素的数量>8时,会自动转化成红黑树

链表阈值

HashMap的链表阈值为:6

static final int UNTREEIFY_THRESHOLD = 6;

在进行删除元素时,如果一个桶中存储元素数量<6之后,会自动跳转为链表

扩容临界值

64

static final int MIN_TREEIFY_CAPACITY = 64;

这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化

节点数组

HashMap中的节点数组就是Entry数组,它代表的就是HashMap中 【数组 + 链表】 数据结构中的 数组
Node数组在第一次使用的时候进行初始化操作,在必要的时候进行resize,resize后的数组的长度扩容为原来的二倍

键值对数量

在HashMap中,使用 size 来表示HashMap中键值对的数量

修改数量

在HashMap中,使用modCount来表示修改次数,主要用于做并发修改HashMap时的快速失败,即:

-fail-fast机制

扩容阈值

在HashMap中,使用 threshold 表示扩容的阈值,也就是 初始容量*负载因子的值
这个问题由 tableSizeFor() 源码解决的

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

HashMap常见问题_第8张图片

负载因子

loadFactory表示负载因子,它表示的是HashMap的密集程度

13.HashMap put全过程

  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)//首次初始化的时候table为null
            n = (tab = resize()).length;//对HashMap进行扩容
        if ((p = tab[i = (n - 1) & hash]) == null)//根据hash值来确认存放的位置。如果当前位置是空直接添加到table中 PS: 多线程情况下可能出现值覆盖
            tab[i] = newNode(hash, key, value, null);
        else { //如果存放的位置已经有值
            Node<K, V> e;
            K k;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))//如果插入位置key重复
                e = p;
            else if (p instanceof TreeNode)//如果插入的位置正好是红黑树
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);//将数据put到红黑树
            else {//如果以上条件均不满足  就说明还是链表
                for (int binCount = 0; ; ++binCount) {//遍历链表
                    if ((e = p.next) == null) { //遍历到链表的尾节点
                        p.next = newNode(hash, key, value, null); //新建一个链表节点(可以看出HashMap链表是尾插的)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 判断链表长度是否大于8(binCount是从0开始)
                            treeifyBin(tab, hash);//如果循环了达到了红黑树的阈值,也就是说这里的链表长度大于8,然后就把这个结构树形化。
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//在循环的过程中遇到了key值相同的节点,跳出循环
                    p = e;//和前面for中的p.next对应,进行循环遍历链表。
                }
            }
            if (e != null) { // existing mapping for key   如果e!=null  说明存在相同的key
                V oldValue = e.value;//记录下原来的key对应的value
                if (!onlyIfAbsent || oldValue == null)//当onlyIfAbsent为false或者旧值为空
                    e.value = value;//替换新的value并返回旧的value
                afterNodeAccess(e);//HashMap中暂无实现
                return oldValue;//返回旧值,这样就处理掉了前面的当key值相同时的情况了。
            }
        }
        ++modCount;// 每次容器发生变化记录
        if (++size > threshold)// 实际大小大于阈值则扩容
            resize();//如果当前HashMap的容量超过threshold则进行扩容
        afterNodeInsertion(evict);//HashMap中暂无实现
        return null;
    }
    final void treeifyBin(Node<K, V>[] tab, int hash) {
        int n, index;
        Node<K, V> e;
        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);//将Node节点变换为TreeNode节点
                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);//将链表树化
        }
    }

HashMap常见问题_第9张图片

扩容机制

14.时间复杂度

当桶中无元素 ->O(1)
当桶中元素为链表时 -> O(1)+O(n)
当桶中元素为红黑树时 -> O(1)+O(logn)

15.如何处理hash碰撞

HashMap底层是使用桶+链表实现的,位桶决定元素的插入位置,位桶是有hash方法决定的,当多个元素的hash计算得到相同的哈希值后,HashMa会把多个Node元素都放在对应的位桶中个,形成链表,这种处理哈希碰撞的方式被称为链地址法

16.HashMap如何get元素

首先会检查 table 中的元素是否为空,然后根据 hash 算出指定 key 的位置。然后检查链表的第一个元素是否为空,如果不为空,是否匹配,如果匹配,直接返回这条记录;如果匹配,再判断下一个元素的值是否为 null,为空直接返回,如果不为空,再判断是否是 TreeNode 实例,如果是 TreeNode 实例,则直接使用 TreeNode.getTreeNode 取出元素,否则执行循环,直到下一个元素为 null 位置。

17.HashMap如何扩容

HashMap 中有两个非常重要的变量,一个是 loadFactor ,一个是 threshold ,loadFactor 表示的就是负载因子,threshold 表示的是下一次要扩容的阈值,当 threshold = loadFactor * 数组长度时,数组长度扩大位原来的两倍,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。

18.数组大小为什么是2的幂次方

查找数组对应的位置需要通过取余计算,HashMap源码中是通过位运算计算数据下标:

int index = hash & (length - 1)

主要是通过key的hash值与数组长度减一做与运算,而要满足于取余预算结果相等的条件,数组长度只能为2的幂次方。

为什么是length-1而不是length

由于length-1一定是基数,因此二进制最后一位永远为1,又是因为与运算,因此最终结果可能为0也可能为1,尽可能的保证分布的松散型,减少hash碰撞

19.HashMap算法

扰动函数

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

HashMap常见问题_第10张图片

右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的地位掺杂了高位的部分特征,这样高位的信息也被变相的保留下来

20.为什么要延迟初始化

new HashMap()的时候只是进行参数传递,真正初始化是在put元素的时候执行resize()
目的:性能优化,节约堆内存

21.树化阈值为什么是8

关键词:泊松分布

LinkedList转化成红黑树非常消耗性能,要尽可能避免
同样我们也希望数据直接落在table中,尽可能少形成链表或者长链表,因此根据泊松分布计算得出(概率学),链表长度达到8的概率已经是极低了,根据源码注释可知,HashMap桶中90%都是0-1长度的链表,性能很高

     /** threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     **/

22.JDK1.8为什么要使用红黑树

历史:JDK7时存在一个bug,Apache Tomcat使用哈希表存储HTTP请求参数时,由于哈希表冲突引发Dos漏洞(服务器仍在运转却拒绝用户访问)
解决方法:主要是因为hash冲突导致链表过长,因此在JDK1.8中引入了红黑树,减少因为链表过长导致的查询速度过慢问题
补充:Tomcat在JDK1.8发布之前采用了一个伤害不大,侮辱性极强的解决方案,引入参数限制处理的参数数量,默认10000

23.HashMap什么情况下会造成死循环

  • 多线程扩容机制导致的,形成了循环链表
  • JDK7中采用了头插法更容易引发
  • JDK8引入红黑树只是解决性能问题,不影响死循环问题

简要描述:扩容会执行链表循环,当Node.next() == null时,跳出循环,多线程会打乱这段逻辑

24.JDK1.7为什么使用头插法

符合优先使用最近的数据原则,新插入的更有可能被用到

25.为什么选择红黑树

HashMap是综合效率最高的数据结构,因此也需要选择综合效率最高的红黑树
平衡二叉树付出很大的代价形成结构,因此采用红黑

26.HashTable

数据结构基本与HashMap相似,只是对map操作的方法都加上了synchronized关键字,全表锁,性能很低

27.ConcurrentHashMap

JDK1.7

分段锁,默认16个分段锁

JDK1.8

引入cas操作:put元素至数组时,采用cas无锁操作,判断链表是否为空
引入synchroized关键字锁住node节点本身

28.什么是ConcurrentHashMap的弱一致性

有可能脏读,读出数据并非实时

你可能感兴趣的:(JDK,java,散列表,哈希算法,hashmap)