HashMap源码解析

对于HashMap,若是是java程序员,那么定然不会陌生,对于HashMap,应该说是最经常使用的一种Map结构了,一样在面试当中也会屡屡被提问到,常见的几种题目:java

  • HashMap的默认容量?
  • HashMap是如何扩容的?
  • HashMap的数组大小为何必定是2的幂?
  • HashMap为何是线程不安全的?
  • Java7到Java8作了哪些改进?为何?

由于重要,因此我也就学习源码,而且将学习心得记录下来,与你们一块儿学习。程序员

首先 再看HashMap以前,咱们来简单回顾一下哈希表面试

哈希表是由一些基于哈希值的桶和链表所构成的。哈希桶就是能够快速检索的数据结构,举个例子 若是要寻找电话本的人的联系方式,咱们能够利用拼音的首字母快速定位到这个联系人,存放这些字母的就叫哈希桶。本质上,哈希桶就是 将一个元素映射成一个能够快速检索的哈希值。算法

哈希桶加上数组就构成了哈希表,数组的好处是随机寻址的速度与长度无关,时间复杂度是O(1),可是哈希表最大的缺点是会发生碰撞,若是多个元素的哈希值是相同的,那么咱们就说哈希值发生了碰撞,为了解决这个问题,咱们将数组换成了链表,找到哈希值时,经过哈希桶里能够精确的找到所要查找的元素。平均差找时间都是O(1)。在java世界中,咱们用来表达哈希表的数据结构就是HashMap。数组

image.png

哈希桶的实现是由hashcode实现的int hashcode 底下有对象:object1,object2....安全

 

咱们先来看java7的HashMap。数据结构

经典的数据结构:数组加链表。函数

经过查看源码 咱们能够知晓HashMap的默认容量为你16,若是改变负载因子 而且将初始值设为17 结果会怎么样么?再次点开源码,咱们会发现他会向上取整为2的幂也就是变成了32.以后咱们能够看到负载因子是0.75.学习

这里有一个问题,就是桶的初始化容量是16个,而咱们的HashCode值是-2^31~2^31-1,42亿个数。那么咱们怎么从任意一个int变成0~n-1。聪明的你确定想到了取余这个想法,可是在这个方法有两个缺点:spa

  • 负数取模仍是负数,因此咱们须要把负数变为正数
  • 速率较慢

那么咱们点开源码发现,为了寻找要插入的元素的索引值,作了一个鬼畜的操做:hash&(length-1),作这样一个位运算,那么这操做正好能够解释为何咱们须要输进去的HashMap的初始容量是2的幂,咱们能够看出hash&(2^n-1),最后的下标就是Hashmap对应的2^n-1的个数下标,之因此把容量定为2的幂,就是由于让2^n-1位运算时拿到的值所有是1,这样作与运算就能够快速找到下标而且分布仍是均匀的(妙啊)。

解释完这个问题 ,咱们来看看是怎么扩容的,根据源码,我么能够看到,当所须要的容量超过原始容量*负载因子(0.75)时,就须要进行扩容,扩容的大小是以前的两倍。而在扩容的过程当中就包含了一个rehash的操做。那么在扩容的时候,会进行transfer也就是将以前的数据进行迁移的操做,遍历全部的元素,而且从新计算哈希值,找到在新表里的索引,把它放进去。这是一个巨坑!!!为了不高位不一样,低位相同,进行了高位与低位的异或操做。

在Java7中的HashMap会有不少问题:

  1. 会碰到死锁
  2. CVE-2011-1858  TOMCAT邮件组的讨论

对于死锁问题,其实多数是程序员本身的问题,由于HashMap自己就不是线程安全的,当在多个线程中使用hashmap的时候,就必须给他加入同步的环境.

对于TOMCAT邮件组的讨论,是说一个安全问题,若是咱们的多个元素映射成同一个哈希值时,会把哈希表退化成一个链表,而链表的查询的时间复杂度的o(n),那么这个查询速度是很可怕的,若是是被黑客利用精心构造的成千上万个元素,具备一样的哈希值,就能够引发Dos攻击,针对这个问题,sun公司提供了一个小补丁,就是若是检查到元素是String的话那么就该用另一种不一样于默认的hash算法来去避免潜在的危险。

此时,针对于这两个问题,咱们迎来了Java8以后的HashMap

Java8对于以前的HashMap以前作了哪些改进呢?

  • 由以前数组/链表--->数组/红黑树
  • 扩容时插入顺序的改进
  • 函数方法
    • foreach
    • compute系列
  • Map的新API
    • merge
    • replace

在Java8的源码中,为何变成树的阈值是8?咱们能够看到对于红黑树的容量服从参数为0.5的泊松分布,大于8的桶中的容量是10万分之一,不易发生碰撞。

在进行put操做时,若是超过阈值,就把桶变为红黑树,若是没超过,仍是用链表来实现。

在进行扩容操做时,掌握了java7时的操做,咱们能够想到,扩容为二倍,要么索引值和以前同样,要么就是最高位是在以前索引值的基础上再加一个一,就至关因而把原先的变成了两个链表,一个是高位的链表,一个是低位的链表,在把它赋值给新的桶中去。从而缓解了死锁问题(生成环的问题)。

相关文章
相关标签/搜索