java基础:HashMap原理

HashMap底层原理:大结构主要使用数组,链表,红黑树(jdk1.8新加)。数组与链表的结合叫哈希表 ,有兴趣的小伙伴可以看数据结构,有详细介绍。

数组:存储的数据是连续的,每一条数据有对应的下标(也即索引),多条数据在数组中,下标是连续的,也即内存地址是连续的,优点是节约空间(也即内存),查询效率高,想象一下图书馆整齐排列的书籍,我们通过检索(相对应数组中的下标)就可以得到相应是书籍,缺点是删除,新增某一条或多条数据效率相对较低,以删除为例,每当删除一条或多条数据是,由于下标是连续不断的,意味着数组需要重新排列,想象一下图书馆中整齐排列的书籍,如果你拿出一本书,那个位置就空缺了,我们就需要移动后面所有的书籍将位置一个个补上,这个是比较消耗时间的。

 

链表:链表是无序(也即不连续)的,每条数据都有指向下一条数据的索引(也即next)    就像一条链子一样将每个珍珠串起来

优点是删除,新增效率高,想象一下,以新增为例,加一条或多条数据就像在链子上加一个珍珠或多个珍珠一样,是不是很容易。

缺点是寻址困难(也即查询效率不高),占用空间大(内存空间),举个例子:如果一样东西随便放,但是房子大小时一定的,那意味着,房子内是不是有很多不连续的没有放东西的空间,这些空间有大有小,现在有一样东西比如自行车,大的空间碎片放浪费,小的放不下,这样是不是造成空间的浪费,由于你放的东西是乱放的,你自己都不知道放哪了,是不是寻址困难,找东西难找。数组相当于你放的东西在哪都有明确的地址,我们可以通过下标找到它,从时间复杂度来看,数组是O(1),而链表是O(n)

总结:数组查询效率高,新增删除低

          链表新增删除效率高,查询效率低

因此我们将数组和链表整合起来,达到新增删除查询效率都高,也即"哈希表",本文的HashMap就是基于"哈希表"实现的一种集合

红黑树:是java1.8新增的一种数据结构,本质是树家族的一种,用来提高HashMap的效率。

 

 

HashMap大结构图:

从上图我们能够看到:

纵向看数组:0-15表示数组的下标,HashMap默认长度是16,所以数组长度为16,数组中存储的是一个个节点(node或者叫Entry) ,Entry数组(Entry[])  是HashMap中核心数组结构,我们也称之为"位桶结构".  

横向看链表:有没有发现一个链表中的一条数据就是一个Entry,Entry中的next指向下一个Entry

Entry:里面有4个属性:hash,key,value,next 看如下源码:

HashMap的存储方式是以key:value形式存储的

key:字面解释"关键",key是唯一的,不能重复   HashMap允许key为null,但只能有一个key为null

value:字面解释"值",  是HasMap中存储的具体值。是可以重复的

next:指向下一个节点(Entry或node)

hash:键(key)对象的hash值

那hash值怎么来的呢?

接下来我们引入一个知识点:hashcode

hashcode:hashcode是Objec类的一个方法,我们都知道java是面向对象的,叫做万物皆对象,因此所有的类都默认继承Object,也即所有类都有这个方法,当调用这个方法时会随机生成32位数字。hashcode不同的两个对象,这两个对象一定不相同,hashcode相同的对象,两个对象不一定相同。这个时候我们就需要使用equals方法进一步比较。

equals:Object类的一个方法,如果hashcode相同,使用equals方法进一步比较时,如果equals也相同,则这两个对象一定相同,如果不相同,则这两个对象一定不想不同。 

我们首先获取key的hashcode,得到32位数字。以下length为数组的长度

jdk1.8之前通过hashcode%length取余运算得到hash值(hash范围:0-数组的长度-1)  由于取余运算效率不高 因此1.8之后改为位运算,位运算是cpu直接支持运行的2进制码,效率极高。但有一个前提,数组的长度必须是2的整数幂。HashMap默认长度是16。

jdk1.8之后通过 hashcode&(length-1) 算出hash值(hash范围:0-数组的长度-1)。

看到这里有没有发现hash值的范围和数组的下标index的范围是一样的,所以,算出hash值就是为了得到要存储数据的位置即下标。看起来很完美,但是有没有发现,如果算出的hash值如果一样(hash冲突),该怎么办?

以hash值为1为例:现在存储数据时,发现有两个或多个key算出的hash值都为1,这个时候,我们要通过equals方法一一比较新增的数据的key对象和原有存储过的对象是否相同,如果相同,就覆盖原有的值(value),如果不相同,就以链表的形式存放

后来,为了减少hash冲突,尽量的散列到位桶的各个位置,引入了二次hash。

说到这里引入一个知识点:负载因子:DEFAULT_LOAD_FACTOR  默认0.75 源码如下

为什么是0.75呢,而不是其他?  默认长度为什么是16?

hashmap 默认长度16*0.75=12,当位桶中存了12个元素时,HashMap就开始扩容,默认两倍 也就是长度32。

选0.75原因是:如果过小,比如0.5 当存储8个数据时就扩容,才用了一半的空间,是不是浪费空间,

                        如果过大,比如1,存了16个数据,总长度才16,是不是容易hash冲突

                        ,

默认长度16的原因:16这个数值特殊,2的4次幂,换成2进制:1111

                                还记不记得hash=hashcode&length     32位的hashcode与1111位运算,真正起作用的hashcode值是后面4位,因为1111前面的28位全部为0,  1111意味着:如下

   例1        1111      默认长度                                       例2         1111                                           例3                1111

                  &                                                                               &                                                                     &

                 1011    hashcode后4位                                            0011                                                                 0111

                  =                                                                               =                                                                     =

                1011                                                                          0011                                                                 0111

以上示例说明,位运算结果值不受1111影响,与hashcode有关,这样就可以大幅度减少hash冲突。

以上为存储数据的原理  获取数据(get)和存储(put)过程大体一样:

1.获取key的hashcode,算出hash值,进而定位到数组中存储的位置

2.在链表上调用equals方法一一比较key对象,直到返回true为止

3.返回key对应的value对象

当链表已经很长了,这个时候查询的效率也会随之降低,这个时候就引入了红黑树

当链表长度达到8,数组长度达到64的时候,对链表进行整改,以红黑树的形式存储。以提高效率。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值