关于android:深入解析HashMap

前言

很快乐遇见你~

HashMap是一个十分重要的汇合,日常应用也十分的频繁,同时也是面试重点。本文并不打算解说根底的应用api,而是深刻HashMap的底层常识,解说对于HashMap的重点常识。

文章整体分为三个局部:第一局部是概述HashMap的重点常识,第二局部是从源码角度剖析HashMap,第三局部解说与HashMap相干的汇合类和一些常见问题。

本文所有源码如若未非凡阐明,均为JDK1.8版本。

根底解析

概述

HashMap是一个映射表的数据结构,他能够存储键值对数据。咱们能够在常数工夫复杂度通过键key找到咱们的值value,是一个十分罕用的Java汇合类。HashMap的继承构造如下:

属于Map汇合体系的一部分,同时继承了Serializable接口能够被序列化,继承了Cloneable接口能够被复制。

HashMap的外围在于计算key下标的hash函数、抵触解决方案以及扩容计划。这也是考验一个映射表体现的三大重点。hash函数决定了key散布是否平均、抵触解决方案决定了在产生大量抵触的状况下是否能保持良好的性能、扩容计划决定了每次扩容是否须要耗费大量的性能。JDK1.8中对HashMap的外部算法进行了全新的降级,让性能失去很大的晋升。

HashMap并不是全能的,对于一些非凡的情景下的需要官网拓展了一些其余的类来满足,如线程平安的ConcurrentHashMap、记录插入程序的LinkHashMap等。

底层构造

jdk1.7及以前采纳的是数组+链表的模式,而1.8之后采纳的是数组+链表或者数组+红黑树的数据结构:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-V2UYbf6M-1606987405698)(https://s3.ax1x.com/2020/12/0…]

红黑树的益处是数据量较大是查问速度比链表快得多,但毛病是在数据量很小时性能优化并不显著甚至倒退,树节点的大小是一般节点的两倍。所以jdk1.8的HashMap做了以下优化:

  • 当链表的长度>=8且数组长度>=64时,会把链表转化成红黑树。
  • 当链表长度>=8,但数组长度<64时,会优先进行扩容,而不是转化成红黑树。

当数组长度较短时,如16,链表长度达到8曾经是占用了最大限度的50%,意味着负载曾经快要达到下限,此时如果转化成红黑树,之后的扩容又会再一次把红黑树拆分均匀到新的数组中,这样非但没有带来性能的益处,反而会升高性能。所以在数组长度低于64时,优先进行扩容。

事实上,得益于HashMap的优良hash算法,链表的长度很少能达到8,在链表较短时,间接采纳链表更加疾速。而在某些极其的状况下,产生了大量的抵触,那么红黑树则能够施展的弱小作用,抗住这种极其状况。

jdk1.8之后HashMap的数组大小管制为2的整数次幂,每次扩容都会扩大到原来的2倍;而jdk1.7数组的大小为素数,且每次扩容之后的大小都是素数。

装载因子

转载因子是一个十分要害的参数。他示意HashMap中的节点数与数组长度之比达到对应阈值后进行数组扩容。例如数组长度是16,装载因子是0.75,那么可包容的节点数是16*0.75=12。那么当往HashMap插入12个数据之后,就会进行扩容。装载因子越大,数组利用率越高,同时产生哈希抵触的概率也就越高;装载因子越小,数组利用率升高,但产生哈希抵触的概率也升高了。所以装载因子的大小须要衡量空间与工夫之间的关系

HashMap中的装载因子的默认大小是0.75,是一个在紧密计算和实际证实比拟适宜的一个值。没有特殊要求的状况下,不倡议批改他的值。

线程平安

HashMap并不是线程平安,所以在多线程的状况下不能间接应用HashMap。能够采纳ConcurrentHashMap类或者Collections.synchronizeMap()办法来保障线程平安。如有多线程同步需要,更加倡议应用ConcurrentHashMap类,他的锁粒度更低,而Collections.synchronizeMap()办法默认锁的是整个Map对象,感兴趣的读者能够自行浏览源码。

那应用了ConcurrentHashMap类是不是相对线程平安?先察看上面的代码:

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("abc","123");

Thread1:
if (map.containsKey("abc")){
    String s = map.get("abc");
}

Thread2:
map.remove("abc");

当Thread1调用containsKey之后开释锁,Thread2取得锁并把“abc”移除再开释锁,这个时候Thread1读取到的s就是一个null了,也就呈现了问题了。所以ConcurrentHashMap类或者Collections.synchronizeMap()办法都只能在肯定的限度上保障线程平安,而无奈保障相对线程平安。

fast-fail

当应用HashMap的迭代器遍历HashMap时,如果此时HashMap产生了结构性扭转,如插入新数据、移除数据、扩容等,那么Iteractor会抛出fast-fail异样,防止出现并发异样,在肯定限度上保障了线程平安。

fast-fail异样只能当做遍历时的一种平安保障,而不能当做多线程并发拜访HashMap的伎俩。若有并发需要,还是须要应用上述的两种办法。

源码解析

要害变量的了解

HashMap源码中有很多的外部变量,这些变量会在上面源码剖析中经常出现,首先须要了解这些变量的意义。

// 存放数据的数组
transient Node<K,V>[] table;
// 存储的键值对数目
transient int size;
// HashMap构造批改的次数,次要用于判断fast-fail
transient int modCount;
// 最大限度存储键值对的数目(threshodl=table.length*loadFactor),也称为阈值
int threshold;
// 装载因子,示意可最大包容数据数量的比例
final float loadFactor;
// 动态外部类,HashMap存储的节点类型;可存储键值对,自身是个链表构造。
static class Node<K,V> implements Map.Entry<K,V> {...}

获取索引

HashMap获取索引次要分为三步:

  • 获取key的hashcode
  • 将hashcode的高16位与低16位进行异或运算,保留原高16位
  • 对运算后果进行取模

前两步的源码能够间接看到hash()办法:

static final int hash(Object key) {
    int h;
    // 获取到key的hashcode,在高下位异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

取模运算在jdk1.7之前的采纳的惯例的求余运算,如13%11==2。为了使键的散布更加平均,jdk1.7管制数组的长度大小为素数。jdk1.8的HashMap数组长度为2的整数次幂,这样的益处是间接对key的hashcode进行求余运算和让hashcode与数组长度-1进行位与运算是雷同的成果。能够看下图帮忙了解:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-J0eaLMED-1606987405699)(https://i.loli.net/2020/12/02…]

但位与运算的效率却比求余高得多,晋升了性能。同时为了使高位也能参加hash散列,把高16位与低16进行异或,使散布更加平均。

取模运算看到putVal()办法,该办法在put()办法中被调用:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    ...
    // 与数组长度-1进行位与运算,失去下标
    if ((p = tab[i = (n - 1) & hash]) == null)
        ...
}

具体的计算过程能够参考下图:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-NQPyFFPE-1606987405700)(https://s3.ax1x.com/2020/12/0…]

增加数值

调用put()办法增加键值对,最终会调用putVal()来真正实现增加逻辑。代码解析如下:

public V put(K key, V value) {
    // 获取hash值,再调用putVal办法插入数据
    return putVal(hash(key), key, value, false, true);
}

// onlyIfAbsent示意是否笼罩旧值,true示意不笼罩,false示意笼罩,默认为false
// evict和LinkHashMap的回调办法无关,不在本文探讨范畴
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    
    // tab是HashMap外部数组,n是数组的长度,i是要插入的下标,p是该下标对应的节点
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 判断数组是否是null或者是否是空,若是,则调用resize()办法进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 应用位与运算代替取模失去下标
    // 判断以后下标是否是null,若是则创立节点直接插入,若不是,进入上面else逻辑
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        
        // e示意和以后key雷同的节点,若不存在该节点则为null
        // k是以后数组下标节点的key
        Node<K,V> e; K k;
        
        // 判断以后节点与要插入的key是否雷同,是则示意找到了曾经存在的key
        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) {
                    p.next = newNode(hash, key, value, null);
                    // 长度大于等于8时转化为红黑树
                    // 留神,treeifyBin办法中会进行数组长度判断,
                    // 若小于64,则优先进行数组扩容而不是转化为树
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到雷同的间接跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 如果找到雷同的key节点,则判断onlyIfAbsent和旧值是否为null
        // 执行更新或者不操作,最初返回旧值
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    // 如果不是更新旧值,阐明HashMap中键值对数量发生变化
    // modCount数值+1示意构造扭转
    ++modCount;
    // 判断长度是否达到最大限度,如果是则进行扩容
    if (++size > threshold)
        resize();
    // 最初返回null(afterNodeInsertion是LinkHashMap的回调)
    afterNodeInsertion(evict);
    return null;
}

代码中对于每个步骤有了具体的解释,这里来总结一下:

  1. 总体上分为两种状况:找到雷同的key和找不到雷同的key。找了须要判断是否更新并返回旧value,没找到须要插入新的Node、更新节点数并判断是否须要扩容。
  2. 查找分为三种状况:数组、链表、红黑树。数组下标i地位不为空且不等于key,那么就须要判断是否树节点还是链表节点并进行查找。
  3. 链表达到肯定长度后须要扩大为红黑树,当且仅当链表长度>=8且数组长度>=64。

最初画一张图总体再加深一下整个流程的印象:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-s8RxksX0-1606987405700)(https://s3.ax1x.com/2020/12/0…]

扩容机制

扩容机制的源码一共分为两个局部:

  • 确定新的数组大小
  • 把旧数组中的元素迁徙到新的数组中

扩容机制中的一个重点是:应用HashMap数组长度是2的整数次幂的特点,以一种更高效率的形式实现数据迁徙

JDK1.7之前的数据迁徙比较简单,就是遍历所有的节点,把所有的节点顺次通过hash函数计算下标,再插入到新数组的链表中。这样会有两个毛病:1、每个节点都须要进行一次求余计算2、插入到新的数组时候采纳的是头插入法,因为尾插入法须要另外保护一个数组,而头插入法会打乱原有的程序

jdk1.8之后进行了优化,起因在于他管制数组的长度始终是2的整数次幂,每次扩大数组都是原来的2倍。带来的益处是key在新的数组的hash后果只有两种:在原来的地位,或者在原来地位+原数组长度。具体为什么咱们能够看下图:

从图中咱们能够看到,在新数组中的hash后果,仅仅取决于高一位的数值。如果高一位是0,那么计算结果就是在原地位,而如果是1,则加上原数组的长度即可。这样咱们只须要判断一个节点的高一位是1 or 0就能够失去他在新数组的地位,而不须要反复hash计算。HashMap源码中并没有一个个地把节点搬到新数组,而是把每个链表拆分成两个链表,再别离插入到新的数组中,这样就能够在不须要开拓一个数组保护尾节点的条件下,实现尾插法,保留原来的节点程序

具体源码剖析如下:

final Node<K,V>[] resize() {
    // 别离是原数组、原数组大小、原阈值;和新数组大小、新最大限度数据量
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // 如果原数组长度大于0
    if (oldCap > 0) {
        // 如果曾经超过了设置的最大长度(1<<30,也就是最大整型负数)
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 间接把阈值设置为最大负数
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 设置为原来的两倍
            newThr = oldThr << 1; 
    }
    
    // 原数组长度为0,但最大限度不是0,把长度设置为阈值
    // 对应的状况就是新建HashMap的时候指定了数组长度
    else if (oldThr > 0) 
        newCap = oldThr;
    // 第一次初始化,默认16和0.75
    // 对应应用默认结构器新建HashMap对象
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果原数组长度小于16或者翻倍之后超过了最大限度长度,则从新计算阈值
    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) {
        // 循环遍历原数组,并给每个节点计算新的地位
        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);
                // 新的地位只有两种可能:原地位,原地位+老数组长度
                // 把原链表拆成两个链表,而后再别离插入到新数组的两个地位上
                // 不必屡次调用put办法
                else { 
                    // 别离是原地位不变的链表和原地位+原数组长度地位的链表
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历老链表,判断新增断定位是1or0进行分类
                    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;
}

相干类

HashTable

Hashtable(留神t是小写)和HashMap的性能根本一样,Hashtable是老一代的java汇合类,他的一个重要的特点就是:线程平安。他的所有须要拜访数据的办法都被synchronize关键字润饰。但这个类曾经被官网锁摈弃不倡议应用,起因有下:

  • Hashtable继承自Dictionary类而不是AbstractMap,类图如下(jdk1.8)

    [外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-HNMFRnQ3-1606987405701)(https://s3.ax1x.com/2020/12/0…]

    Hashtable诞生的工夫是比Map早,属于第一批的java汇合类。而为了兼容新的汇合在jdk1.2之后也继承了Map接口,Map接口蕴含了Dictionary中的办法。而Dictionary接口在目前曾经齐全被Map取代了,所以更加倡议应用继承自AbstractMap的HashMap。

  • Hashtable的设计曾经落后了。Hashtable的底层构造采纳的是数据+链表,初始数组大小是11,后续扩容为2n+1(n是原数组大小),保障数组长度是素数,通过间接求余来失去数组下标。 在jdk1.8之后HashMap采纳高下位异或+位与代替取模的优良设计进步哈希散列性能,应用红黑树来进步在极其抵触状况下的抗压能力。而这些在Hashtable中都没有用到,性能体现不如HashMap。
  • Hashtable被设计为线程平安,然而锁粒度太大性能较低,相比ConcurrentHashMap后者的锁粒度更低,性能更佳。且在大部分状况下,并不波及到并发操作,所以频繁加锁解锁是没有必要的。所以失常状况下应用HashMap更佳,而如果有并发需要,应用ConcurrentHashMap是更好的抉择。

ConcurrentHashMap

HashMap无奈保障在多线程并发拜访下的平安,而ConcurrentHashMap就是为了解决这个问题。ConcurrentHashMap并不是和Hashtable一样采纳间接对整个数组进行上锁,而是对数组上的一个节点上锁,这样如果并发拜访的不是同个节点,那么就无需期待开释锁。如下图:

不同线程之间的拜访不同的节点不相互烦扰,进步了并发拜访的性能。当然,如果拜访同个节点,还是须要期待上一个线程开释锁。

这是jdk1.8优化之后的设计构造,jdk1.7之前是分为多个小数组,锁的粒度比Hashtable稍小了一些。如下:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-DJgNyffa-1606987405702)(https://s3.ax1x.com/2020/12/0…]

锁的是Segment,每个Segment对应一个数组。而jdk1.8之后锁的粒度进一步升高,性能也进一步提高了。

LinkHashMap

HashMap是无奈记住插入程序的,在一些须要记住插入程序的场景下,HashMap就显得无能为力,所以LinkHashMap就应运而生。LinkHashMap外部新建一个外部节点类LinkedHashMapEntry继承自HashMap的Node,减少了前后指针。每个插入的节点,都会应用前后指针分割起来,造成一个链表,这样就能够记住插入的程序,如下图:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-xynLOdZH-1606987405703)(https://s3.ax1x.com/2020/12/0…]

图中的红色线示意双向链表的援用。遍历时从head登程能够依照插入程序遍历所有节点。

TreeMap

HashMap的key排列是无序的,hash函数把每个key都随机散列到数组中,而如果想要放弃key有序,则能够应用TreeMap。TreeMap的继承构造如下:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-K7DyXWLe-1606987405703)(https://s3.ax1x.com/2020/12/0…]

他继承自Map体系,实现了Map的接口,同时还实现了NavigationMap接口,该接口拓展了十分多的不便查找key的接口,如最大的key、最小的key等。

TreeMap尽管领有映射表的性能,然而他底层并不是一个映射表,而是一个红黑树。他能够将key进行排序,但同时也失去了HashMap在常数工夫复杂度下找到数据的有点。所以若不是有排序的需要,惯例状况下还是应用HashMap。

小结

这几个类补救了HashMap在一些非凡的状况下的能力,如线程平安、记住插入程序、给key排序。个别状况下如若没有特殊要求,应用HashMap即可。

常见问题

这部分总结一些常见的问题,问题的答案在文章中都有波及到,也当为全文回顾。

为什么jdk1.7以前管制数组的长度为素数,而jdk1.8之后却采纳的是2的整数次幂?

答:素数长度能够无效缩小哈希抵触;JDK1.8之后采纳2的整数次幂是为了进步求余和扩容的效率,同时联合高下位异或的办法使得哈希散列更加平均。

为何素数能够缩小哈希抵触?若能保障key的hashcode在每个数字之间都是均匀分布,那么无论是素数还是合数都是雷同的成果。例如hashcode在1~20均匀分布,那么无论长度是合数4,还是素数5,散布都是平均的。而如果hashcode之间的距离都是2,如1,3,5…,那么长度为4的数组,地位2和地位4两个下标无奈放入数据,而长度为5的数组则没有这个问题。长度为合数的数组会使距离为其因子的hashcode汇集呈现,从而使得散列成果升高。具体的内容能够参考这篇博客:算法剖析:哈希表的大小为何是素数,这篇博客采纳数据分析证实为什么素数能够更好地实现散列。

为什么数组长度是2的整数次幂?HashMap如何保障数组长度肯定是2的整数次幂呢?

答:不便取模和扩容,进步性能;默认数组为16长度,每次扩容都是为原来的2倍,若初始化指定非2的整数次幂,会取刚好大于该指定大小的最小2的整数次幂。

第一个问题在后面讲过,这里不再赘述,第二个问题要联合源码来剖析,当咱们初始化指定一个非2的整数次幂长度时,HashMap会调用tableSizeFor()办法来使得长度为2的整数次幂:

public HashMap(int initialCapacity, float loadFactor) {
    ...
    this.loadFactor = loadFactor;
    // 这里调用了tableSizeFor办法
    this.threshold = tableSizeFor(initialCapacity);
}

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

tableSizeFor()办法使得最高位1后续的所有位都变为1,最初再+1则失去刚好大于initialCapacity的最小2的整数次幂数。可参考下图了解:

[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-3P5iV7jM-1606987405704)(https://s3.ax1x.com/2020/12/0…]

这里应用了8位进行模仿,32位也是同理。所以当咱们在构造函数中指定HashMap的长度并不是2的整数次幂时,他会主动变换成2de整数次幂。如输出5,会变成8,输出11会变成16。

为什么装载因子是0.75?可不可以换成其余数值?

答:0.75是在谨严的数学计算以及衡量工夫和空间得出的性价比最高的后果,个别状况下并不倡议改变。

个别状况下,咱们总是想要装载因子越高越好,能够进步数组的利用率。但装载因子太高带来的问题是产生激烈的哈希抵触,性能急剧下降,抵触数量的减少在0.75这个拐点之后呈指数爆炸增长,而0.75则刚好是在保障工夫复杂度在低水平的状况下尽可能大地利用数组资源。在HashMap中有这样一段正文:

/*...
* 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
*...
*/

他示意的是装载因子是0.75的状况下,在实践概率中一个数组节点中链表长度的可能性,能够看到长度为8的可能性曾经是非常低了。而事实的数据可能并不合乎均匀分布,所以红黑树的存在次要是为了解决极其状况下的数据压力。

那转载因子可不可以换成其余数字?没有非凡的要求不要更换。调低了工夫复杂度升高不显著然而却会带来空间复杂度的晋升,调高了空间复杂度晋升不显著带来的却是工夫复杂度的激烈晋升。更加具体的剖析能够参考这篇文章:面试官:为什么 HashMap 的加载因子是0.75?,作者通过数学分析来阐明为什么0.75是最好的。

为什么插入HashMap的数据须要实现hashcode和equals办法?对这两个办法有什么要求?

答:通过hashcode来确定插入下标,通过equals比拟来寻找数据;两个相等的key的hashcode必须相等,但领有雷同的hashcode的对象不肯定相等。

这里须要辨别好他们之间的区别:hashcode就像一个人的名,雷同的人名字必定相等,然而雷同的名字不肯定是同集体;equals比拟内容是否雷同,个别由对象笼罩重写;“==”判断的是援用地址是否雷同。

HashMap中须要应用hashcode来获取key的下标,如果两个雷同的对象的hashcode不同,那么会造成HashMap中存在雷同的key;所以equals比拟雷同的key他们的hashcode肯定要雷同。HashMap比拟两个元素是否雷同采纳了三种比拟办法联合:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))) 。对于更加深刻的解说能够参考这篇文章:[Java进步篇——equals()与hashCode()办法详解](https://www.cnblogs.com/qian1…,作者十分具体地分析了这些办法之间的区别。

最初

对于HashMap的内容很难在一篇文章讲完,其余发散的知识点读者有趣味能够深刻去理解。HashMap是一种映射表,他的重点在于散列表这种底层的数据结构,装载因子的设计应用了概率论、高等数学等基础学科,把握好数据结构数学等基本知识十分重要。同时,HashMap的设计衡量了泛滥因素,晋升性能,在jdk1.7晋升到1.8能够非常明显看到这点,这些是十分值得咱们学习的。

全文到此,原创不易,感觉有帮忙能够点赞珍藏评论转发。
笔者满腹经纶,有任何想法欢送评论区交换斧正。
如需转载请评论区或私信交换。

另外欢迎光临笔者的集体博客:传送门

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理