Hash算法及java HashMap底层实现原理理解(含jdk 1.7以及jdk 1.8)

如今不少公司面试都喜欢问java的HashMap原理,特在此整理相关原理及实现,主要仍是由于不少开发集合框架都不甚理解,更不要说各类其余数据结构了,因此形成面子造飞机,进去拧螺丝。html

1.哈希表结构的优点?

哈希表做为一种优秀数据结构
本质上存储结构是一个数组,辅以链表和红黑树
数组结构在查询和插入删除复杂度方面分别为O(1)和O(n)
链表结构在查询和插入删除复杂度方面分别为O(n)和O(1)
二叉树作了平衡 二者都为O(lgn)
而哈希表二者都为O(1)

2.哈希表简介

哈希表本质是一种(key,value)结构 由此咱们能够联想到,能不能把哈希表的key映射成数组的索引index呢? 若是这样作的话那么查询至关于直接查询索引,查询时间复杂度为O(1) 其实这也正是当key为int型时的作法 将key经过某种作法映射成index,从而转换成数组结构 

3.数据结构实现步骤

1.使用hash算法计算key值对应的hash值h(默认用key对应的hashcode进行计算(hashcode默认为key在内存中的地址)),获得hash值
2.计算该(k,v)对应的索引值index 
  索引值的计算公式为 index = (h % length) length为数组长度
3.储存对应的(k,v)到数组中去,从而造成a[index] = node<k,v>,若是a[index]已经有告终点
便可能发生碰撞,那么须要经过开放寻址法或拉链法(Java默认实现)解决冲突

固然这只是一个简单的步骤,只实现了数组 实际实现会更复杂
hash表 数组相似下图java

索引 0 1 2 3 4 5 6 7
--- null null <10,node1> <27,node2> null null null null
---

jdk 1.7以及以前的结构相似以下:node

jdk 8中的结构以下:面试

 

 

两个重要概念算法

哈希算法
h 经过hash算法计算获得的的一个整型数值 
h能够近似看作一个由key的hashcode生成的随机数,区别在于相同的hashcode生成的h必然相同
而不一样的hashcode也可能生成相同h,这种状况叫作hash碰撞,好的hash算法应尽可能避免hash碰撞
(ps:hash碰撞只能尽可能避免,而没法杜绝,因为h是一个固定长度整型数据,原则上只要有足够多的输入,就必定会产生碰撞)
关于hash算法有不少种,这里不展开赘述,只须要记住h是一个由hashcode产生的伪随机数便可
同时须要知足key.hashcode -> h 分布尽可能均匀(下文会解释为什么须要分布均匀)
能够参考https://blog.csdn.net/tanggao1314/article/details/51457585
解决碰撞冲突

由上咱们能够知道,不一样的hashcode可能致使相应的h即发生碰撞
那么咱们须要把相应的<k,v>放到hashmap的其余存储地址数组

解决方法1:Hash冲突的线性探测开放地址法
经过在数组以某种方式寻找数组中空余的结点放置
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时
以p为基础,产生另外一个哈希地址p1,若是p1仍然冲突,再以p为基础,产生另外一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,
解决方法1:链地址法(JDK采用的哈希冲突解决方法及JDK7的源码,JDK8差别大)
经过引入链表 数组中每个实体存储为链表结构,若是发生碰撞,则把旧结点指针指向新链表结点,此时查询碰撞结点只须要遍历该链表便可
在这种方法下,数据结构以下所示
int类型数据 hashcode 为自身值
 
链地址法示例图

在JAVA中几个细节点

1.为何须要扩容?扩容因子大仍是小好?bash

因为数组是定长的,当数组储存过多的结点时,发生碰撞的几率大大增长,此时hash表退化成链表数据结构

过大的扩容因子会致使碰撞几率大大提高,太小扩容因子会形成存储浪费,在Java中默认为0.75多线程

2.当从哈希表中查询数据时,若是key对应一条链表,遍历时如何判断是否应该覆盖?框架

当遍历链表时,若是两个key.hashcode的h一致会调用equals()方法判断是否为同一对象,equal的默认实现是比较二者的内存地址

所以为何Java强调当重写equals()时须要同时重写hashcode()方法,假设两个不一样对象,在内存中的地址不一样分别为a和b,那么重写equals()之后a.equals(b) =true 开发者但愿把a,b这两个key视做彻底相等
然而因为内存地址的不一样致使hashcode不一样,会致使在hashmap中储存2个本应相同的key值

这里提供一个范例
public class Student { //学号 public int student_no; //姓名 public String name; @Override public boolean equals(Object o) { Student student = (Student) o; return student_no == student.student_no; } } 

一般状况下咱们像上图同样指望经过判断两个Student的学号是不是否为同一学生
然而在使用map或set集合时产生出乎意料的结果

 

 
image.png

当咱们重写hashcode()时

@Override public int hashCode() { return Objects.hash(student_no); } 

能够看到如今能够正常使用集合框架中的一些特性

 

 
image.png

3.为何在HashMap中数组的长度length = 2^n(初始值为16),即2的n次 ?

当计算索引值index = h % length 因为计算机的取余操做速度很慢,而计算机的按位取余 & 的操做很是快,又由于 h%length = h & (length-1) (须要知足length = 2^n) 所以规定了length = 2^n 加快index的计算速度,所以是利用了计算机自己的计算特性

4.HashMap的红黑树在哪里体现呢?

红黑树是JDK8中对hashmap做的一个变动,在JDK8以前,HashMap、HashSet采用数组+链表的形式来解决哈希冲突,咱们知道优秀的hash算法应避免碰撞的发生,但假如开发者使用了不合适的hash算法,O(1)级别的数组查询会退化到O(n)级链表查询,所以在JDK8中引入红黑树的,当一个结点的链表长度大于8时,链表会转换成红黑树,提升查询效率,而链表长度小于6时又会退化成链表

5.扩容是如何触发的?

当hashmap中的size > loadFactory * capacity即会发生扩容,size 也是数组结点和链表结点的总和,要明确扩容是一个很是耗费性能的操做,由于数组的长度发生改变,须要对全部结点的索引值从新进行计算,而在JDK8中对这部分进行了优化,详细能够参考https://blog.csdn.net/aichuanwendang/article/details/53317351,在扩容完后减轻了碰撞产生的影响。可是值得注意的是若是两个线程都发现HashMap须要从新调整大小了,它们会同时试着调整大小。在调整大小的过程当中,存储在链表中的元素的次序会反过来,由于移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了不尾部遍历(tail traversing)。若是条件竞争发生了,那么就死循环了。因此多线程环境要使用ConcurrentHashMap而不能使用HashMap。

在jdk 8中,对扩容进行了优化,增长了高16位异或低16位,此时当n变为2倍时,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。若是没有变,意味着不少不须要移动,具体可参见源代码中hash方法的实现,也能够参考https://my.oschina.net/u/2307589/blog/1800587的示意图,画的很清晰。

在正常的Hash算法下,红黑树结构基本不可能被构造出来,根据几率论,理想状态下哈希表的每一个箱子中,元素的数量遵照泊松分布通俗易懂的解释泊松分布

 

 

(即除非hash算法有问题,不然单位时间内发生冲撞的几率是能够估算出来的):

P(X=k) = (λ^k/k!)e^-λ,k=0,1,...

当负载因子为 0.75 时,上述公式中 λ 约等于 0.5,所以箱子中元素个数和几率的关系以下:(参考https://blog.csdn.net/Philm_iOS/article/details/81200601),下述分布来自源码文档

> * Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * 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

最后和JDK 7不一样的是,JDK1.8中新增了一个实现了Entry接口的内部类Node<K,V>,即哈希节点。

参考:

JDK 8中hashmap的实现解析:https://blog.csdn.net/lch_2016/article/details/81045480

相关HashMap相关的面试问题:https://blog.csdn.net/suifeng629/article/details/82179996

相关文章
相关标签/搜索