当前位置: 首页 > 后端技术 > Java

认识一下HashMap的真面目

时间:2023-04-01 23:57:31 Java

HashMap是java中使用最广泛的集合类之一,它以key/value(键值对)的形式保存数据。你可以称HashMap为集合类,也可以称其为容器。java中的很多容器框架,比如Spring,其实都是用HashMap来存储数据的。当然,java坚持“一切皆对象”的原则。当然,对象存储在HashMap中,但它们是由“键值”对组成的对象。HashMap继承于AbstractMap并实现了Map接口。因此,想要彻底理解HashMap,还是要从Map接口和AbstractMap入手。其实Map接口和Map接口没有任何关系,只是一个接口。定义通用的Map类方法,如size()、isEmpty()、get()、put()、containsKey()、containsValue()...等。比较重要的是Map接口定义了一个Entry内部接口。这个Entry其实就是Map中包含的对象,不同的Map实现类会有不同的实现。Map接口还实现了几个方法,暂时不详细分析。这其实很好的回答了“接口能否实现方法?”这个问题。AbstractMap抽象类AbstractMap抽象类实现了Map接口,并体现了Map接口定义的一些方法。实现了一个叫SimpleEntry的Entry(也就是定义在Map接口中的内部接口),和一个叫SimpleImmutableEntry的Entry。暂时不影响主题:识破HashMap的真面目。HashMap的数据结构回去继续研究HashMap。首先,识别HashMap的数据结构。我们先从简单的开始,循序渐进,先易后难,循序渐进。让我们先了解一下Node。Node是Entry的实现,数据结构很简单:finalinthash;最后的K键;V值;下一个节点;hashvalue,key,vaue和指向下一个节点的简单链表结构。HashMap桶中的Node链表容量增加后,简单链表的结构会自动修改为红黑树。本文暂时不研究红黑树。表数组表数组是HashMap真正存储数据的地方,所以说白了,HashMap的底层其实就是一个数组。但是,HashMap的表是一个特殊的数组。数组中的每一个对象其实就是我们常说的一个桶。桶中包含Node对象,这些对象是我们放入HashMap中的键值对。HashMap的初始化HashMap提供了几种不同的初始化方法。不同的是有没有初始化的capacity,有没有初始化的object,有没有初始化的loadFactor和threshold。这些概念需要我们慢慢地一一去理解。容量是HashMap中表数组的大小。HashMap的容量是2的n次方,如果初始化时没有设置容量,则默认为16。如果初始化时设置了initialCapacity,则HashMap的容量是最接近initialCapacity且大于的整数比initialCapacity的2。Power,例如,如果initialCapacity设置为3、4、5,则HaspMap的最终容量为8,如果设置为9、10、11……,则HashMap的最终容量为16,以此类推。这个容量的计算是通过tableSizeFor方法实现的,我们暂时按下好奇心(为什么这个方法可以实现大于输入参数cap的最接近2的整数次幂?)。staticfinalinttableSizeFor(intcap){intn=cap-1;n|=n>>>1;n|=n>>>2;n|=n>>>4;n|=n>>>8;n|=n>>>16;返回(n<0)?1:(n>=MAXIMUM_CAPACITY)?最大容量:n+1;}loadFactor直译为加载因子,意译为膨胀因子,初始值设置为0.75。thresholdthreshold是扩容阈值,HashMap的容量*loadFactor=threshold。当HashMap中存储的对象数量增长到阈值时,扩容。比如HashMap的初始容量是16,默认的loadFactor是0.75,那么threshold=16*0.75=12。然后当HashMap存储对象的个数(size)大于12时,HaspMap调用resize()方法自动扩容。4.size可以通过调用size()方法得到,HashMap中实际存储的对象个数。要想看清HashMap的真面目,最好对size和capacity有一个清晰的认识。大小是应用程序的宝贵数据。我们在开发过程中提到的HashMap的大小,其实就是指大小。而Capacity对应用没有任何意义,只是HashMap内部使用的一个概念,只有对HashMap的内部结构感兴趣,想研究清楚其工作机制的同学才有意义。HashMap赋值HashMap的赋值逻辑如下(假设要存储的数据为e):如果查表数组为空,则初始化表数组指定容量或默认容量并计算根据key1的hash值(算法为(capacity-1)&hash(key1))对应bucket。这一步非常重要。一般来说,一个优秀的哈希算法可以保证不同的键值尽可能得到不同的哈希值,也可以保证它们被放在不同的桶中。但难免会出现不同key值得到相同hash值的情况(hash冲突:key1<>key2,hash(key1)=hash(key2)),这种情况下会被放到同一个bucket中(如表[5])。拿到bucket之后判断bucket中是否有数据。如果没有数据,直接新建一个Node:newNode(hash,key1,value1,null),放入桶中,结束。否则,如果桶中有数据,则有两种情况:一种是key值key1重复赋值,另一种是hash冲突。如果是hash冲突,则新建一个Node:newNode(hash,key1,value1,null)并设置为bucket中的最后一个Node。如果是重复赋值(bucket中数据的key值=key1),则对key1重新赋值value1,返回key1的旧值。所以我们可以看到对于key,HashMap是不重复的,也就是说同一个key在HashMap中只保留了一份数据。而且,一般情况下,一个HashMap的桶中只保存一个对象。只有发生哈希冲突后,才可能在桶中放置多个对象,以链表结构存储。HashMap中的null对象这里的null对象指的是HashMap中key值为null的Node对象。大家都知道HashMap允许且只能存储key=null的对象,如代码:HashMaphm=newHashMap();hm.put(null,"它是空的");hm.put(null,"我是空的");hm.put(null,"null喜欢null");最终hm容器中只有一个null对象,hm.get(null)应该得到的是“nullloavesnull”。这背后的原因可以从上面的HashMap赋值逻辑中找到,只要知道null的hash值为0,就很容易得出上面的结论。通过get(key)方法从HashMap中获取数据的逻辑如下(假设获取的数据为key=key1):如果表数组不为空且数组长度大于0,则使用和put数据一样的算法得到key1bucket对应的value。如果桶不为空,则从第一个节点开始检查。如果节点的键值等于key1,则返回节点的值。如果第一个节点不满足条件,则依次检查桶中的所有其他节点。如果桶为空,或者桶不为空但没有找到满足条件的对象(应该不可能),则返回null,表示当前HashMap中不存在key值为key1的对象。需要注意的是校验节点的key值等于key1。逻辑是:两个对象相等,或者两个对象都不为空且key1.equals(key)。远胜于!相信HashMap的神秘面纱的一角已经被揭开。