Hashmap底层原理
什么是散列法:
散列法(Hashing)是一种将字符组成的字符串转换为固定长度(一般是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法。
由于通过更短的哈希值比用原始值进行数据库搜索更快,这种方法一般用来在数据库中建立索引并进行搜索,同时还用在各种解密算法中。
什么是HashMap:
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 内部结构:
可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分key(键)、value(值),next(指向下一个Entry),通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。如果链表大小超过树形转换的阈值(TREEIFY_THRESHOLD= 8),链表就会被改造为树形结构。
因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是
红黑树是平衡二叉树,在查找性能方面比链表要高
.
![](https://i-blog.csdnimg.cn/blog_migrate/fbc5c4f03baf4e815eb610f0e138406a.jpeg)
查询时间复杂度:
HashMap的本质可以认为是一个数组,数组的每个索引被称为桶,每个桶里放着一个单链表,一个节点连着一个节点。很明显通过下标来检索数组元素时间复杂度为O(1),而且遍历链表的时间复杂度是O(n),所以在链表长度尽可能短的前提下,HashMap的查询复杂度接近O(1)
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,插入和删除容易。
HashMap的工作原理 :
HashMap是基于散列法(又称哈希法)的原理,使用put(key, value)存储对象到HashMap中。
1、
当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置
在Object类中有一个方法:
public native int hashCode();
该方法用native修饰,所以是一个本地方法,所谓本地方法就是非java代码,这个代码通常用c或c++写成,在java中可以去调用它。
调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关.
2、
如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖
3、
新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
4、
如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部
HashCode:
什么是哈希码:在Java中,哈希码代表对象的特征。
哈希码产生的依据:哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。
String的hashcode方法
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
选择31的理由:
第一,31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。
第二、数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。31可以被 JVM 优化,31 * i = (i << 5) - i
equals 以及 ==拓展:
equals方法是java.lang.Object类的方法
1、对于字符串变量
'=='比较的是变量本身的值,即在内存中的首地址
(java中,对象首地址是它在内存中存放的起始地址,后面的地址是各个属性的地址)
'equals()'比较字符串中包含的内容是是否相同
String的equals方法因为重写了Object的equals方法,所以可以比较字符串的内容,而StringBuffer因为没重写equals方法,直接继承了Object的equals方法,所以比较的是地址。
例子:
String s1,s2,s3 = "abc", s4 ="abc" ;
s1 = new String("abc");
s2 = new String("abc");
s1==s2 是 false //两个变量的内存地址不一样,也就是说它们指向的对象不 一样,
s1.equals(s2) 是 true //两个变量的所包含的内容是abc,故相等。
2、对于非字符串
equals方法对于字符串来说是比较内容的,而对于非字符串来说是比较,其指向的对象是否相同的。
== 比较符也是比较指向的对象是否相同的也就是对象在对内存中的的首地址。
3、基本类型比较
(字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double)
只能用==比较,不能用equals
4、包装类比较
(Character、Boolean、Byte、Short、Integer、Long、Float、Double)
装箱:基本数据类型转换为包装类
拆箱:包装类转换为基本数据类型
==是比较地址的,equals是比较内容的
hashcode()拓展:
为什么还要用equals和hashcode一起比较?
重写的equal比较的比较全面比较复杂,效率比较低;hashcode只需要生成一个hash值进行比较就可以了,效率很高。
hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
解决方案是:每当需要对比的时候,首先用hashcode先去对比,如果hashcode不一样,则表示这两个对象肯定不相等,如果hashcode相同,再比较它们的equal,如果相同,则真相同,提高了效率。
HashMap的扩容:
HashMap中有两个重要的参数:初始容量大小和加载因子(
loadFactor的默认值为0.75),初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容。
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。