前言

  在我们开发中,HashMap是我们非常常用的数据结构,接下来我将进一步去了解HashMap的原理、结构。

1、HashMap的实现原理

  HashMap底层是基于Hash表(也称“散列”)的数据结构实现的,由数组链表组成,数组是HashMap的主体,链表主要是为了解决哈希冲突而存在的。

  数组里每个地方都存了Key-Value这样的实例,在Java7中叫 Entry,在Java8中叫 Node

  他本身所有的位置都是 null,在 put 插入的时候会根据 key 的 hash 值去计算一个 index 值。

  例如,我 put(“兄弟”,“砍我”),我插入了为“兄弟”的元素,这个时候我们会同通过哈希函数计算插入的位置,计算出来的 index 是2,那结果如下。

hash(“兄弟”)=2

  

  以上就是我们前面说到的,数组是HashMap的主体。而为什么需要用到链表,这就需要提到哈希冲突了。

  我们都知道数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,就是“兄弟”和“弟兄”我们都去hash,有一定的概率会一样,这就出现我们说的哈希冲突,就像上面的情况,我再次哈希“弟兄”极可能会hash到一个值上,这就形成了链表。

  每一个节点都会保存自身的 hash、key、value、以及下个节点,我们看看 Node 的源码。

2、关于链表,新的Entry节点插入链表的方式

  新增一个Entry节点,在 Java8 之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子一样,新增的"弟兄"会代替“兄弟”的位置,因为写这个代码的坐着认为后来的值被查找的可能性更大,有利于提升查找到的效率。

  但是,在Java8之后,都是使用尾部插入法。至于为何使用尾插法,这就跟我们的扩容机制有关了。

3、HashMap的扩容机制

  前面我们提过,数组容量是有限的,数据多次插入,到达一定数量就会进行扩容,也就是resize

  而扩容的时机主要取决于两个因素:

  • Capacity:HashMap当前长度;
  • LoadFactor:负载因子,默认值是 0.75f 。

  这个比较好理解,比如我们当前容量大小是100,当你存进第76个的时候,判断发现需要进行 resize 了,那就进行扩容,但是HashMap的扩容也不是简单地扩大容量这么简单的。

  HashMap的扩容分为两步

  1. 扩容:创建一个新的 Entry 空数组,长度是原数组的2倍;
  2. ReHash:遍历原 Entry 数组,把所有的 Entry 重新 Hash 到新数组。

  有的朋友会问,为何要重新Hash,直接复制过去它不香吗?

  这是因为长度扩大以后,Hash 的规则也随之改变。Hash 的公式如下:

index = HashCode(key)&(Length - 1)

  原来长度(Length)是8,你位运算出来的值是2,新长度是16,你位运算出来的值明显不一样了。

  扩容前

  扩容后

4、为何之前用头插法,Java8之后改用尾法了?

  我们先举个例子,我们现在我那个一个容量大小为2的put两个值,负载因子是0.75,那么在我们put第二个的时候进辉进行resize。

  2 * 0.75 = 1,所以插入第二个就要 resize 了。

   现在我们要在容量为2的容器里面用不同的线程插入A、B、C,假如我们在 resize 之前打个断点,那意味着数据都插入了,但是还没有 resize ,那扩容前可能是这样的。

  我们可以看到链表的指向:A --> B --> C

 Tip : A的下一个指针是指向B的。

  以为 resize 的赋值方式,也就是使用了单链表的头插入方式,同一位置上的新元素总会被放在链表的头部位置,在旧数组中同一条 Entry 链上的元素,通过重新计算索引位置后,有可能放到了新数组的不同位置上。

  就可能出现下面的情况,你发现问题了没有?B的指针指向了A。

  一旦几个线程都调整完成,就可能出现环形链表。

  这个时候再去取值,悲剧就出现了 —— Infinite Loop;

5、那JDK1.8的尾插是怎样的?

  在 Java8 之后的链表引入了红黑树的部分,我们可以看到代码已经多了很多 if else 的逻辑判断,红黑树的引入巧妙地将原本 O(n)的时间复杂度降低到 O(logn)。

  Tip:红黑树的部分也很重要,面试中经常会被问到,在今后写到数据结构的时候再讲。

  使用头插法会改变链表上的顺序,但是如果使用尾插,在扩容时会保持链表原本的顺序,就不会出现链表成环的问题。

  就是说,原本指向 A-->B,在扩容之后那个链表还是 A-->B。

  Java7 的多线程操作 HashMap 时可能引起死循环,原因是扩容转移后,前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

  Java8 在同样的前提下并不会引起死循环,原因是扩容转移前后链表的顺序不变,保持之前节点的引用关系。

6、HashMap多线程的应用

  上面提到,Java8不会引起死循环,是不是意味着可以把 HashMap 用在多线程中?

  我认为,即使不会出现死循环,但是通过源码看到 put/get 方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒 put 的值,下一秒 get 的时候还是原值,所以线程安全还是无法保证。

7、HashMap的默认初始化长度

  在源码中有提示,初始化大小是16。在JDK1.8 的 236 行,这么写着 1<<4就是16,这里为何运用了位运算呢?直接写16不香吗?

  因为我们在创建 HashMap 的时候,阿里规范插件会提醒我们最好赋初值,而且最好是 2 的幂。

  这样是为了位运算的方便,位运算比算数计算的效率高了很多,之所以选择16,是为了服务将 Key 映射到 index 的算法。

  前面说过了,所有的 Key 我们都会拿到它的 hash 值,但是我们怎么尽可能地得到一个均匀分布的 hash 值呢?

  这里就需要我们通过 Key 的 HashCode 值去做位运算。

  例如,key为“兄弟”的十进制为669275,那二进制就是10100011011001011011。

String key = "兄弟";
int hashCode = key.hashCode();
//669275

  我们再看下index的计算公式:index = HashCode(Key)&(Length - 1)

index = (n-1)&hash

  15的二进制是 1111,那 10111011000010110100 &1111 十进制就是4。

  之所以用位与运算效果取模一样,性能也提高了不少!

8、那为何用16,而不是其他的?

  因为在使用不是2的幂的数字是,Length - 1 的值是所有二进制位全为1,这种情况下,index 的结果等同于 HashCode 后几位的值。

  只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

  这是为了实现均匀分布。

9、我们重写equals方法的时候,为什么需要重写hashCode方法?

  在Java中,所有的对象都是继承于Object类。Object类中有两个方法 equals、hashCode,这两个方法都是用来比较两个对象是否相等的。

  在未重写 equals 方法,我们是继承了 Object 的equals方法,那里的 equals是比较两个对象的内存地址,显然我们 new 了2个对象,内存地址肯定不一样。

  • 对于值对象,==比较的是两个对象的值;
  • 对于引用对象,比较的是两个对象的地址;

  是否还记得前面说过的HashMap是通过 key  的hashCode去寻找index的,那index一样就形成了链表了,也就是说“兄弟”和“弟兄”的index都可能是2,在一个链表上。

  我们去 get 的时候,他就是根据 key 去 hash,然后计算出 index,找到了 2,那我怎么找到具体的“兄弟”还是“弟兄”呢?

  equals!!!是的,所以如果我们对 equals 方法进行了重写,建议一定要对 hashCode 方法重写, 以保证相同的对象返回相同的 hash 值,不同的对象返回不同的 hash 值。

  不然一个链表的对象,你怎么知道你要找哪个?到时候发现 hashCode 都一样,这不完犊子了嘛。

10、既然前面说到HashMap是线程不安全的,那我们应该怎么处理HashMap在线程安全的场景呢?

  在这样的场景,我们一般都会使用 HashTable 或者 CurrentHashMap,但是因为前者的并发度的原因,基本上没什么使用场景,所以存在线程不安全的场景,我们都是用的是CurrentHashMap。

  我看过 HashTable 的源码,非常简单、粗暴,直接在方法上加锁,并发度很低,最多同时允许一个线程访问;CurrentHashMap 就好很多了, 1.7 和 1.8 有较大的不同,不过并发度都比前者好很多。

10、总结

HashMap 绝对是最常问的集合之一,基本上所有的点都要烂熟于心

下面引入几个常见的HashMap面试题(答案后面再补)

问一:HashMap的底层数据结构?

  答:

问二:HashMap的存取原理?

  答:

问三:Java7 和 Java8 的区别?

  答:

问四:为什么HashMap是线程不安全的?

  答:

问五:有什么线程安全的类代替吗?

  答:

问六:默认初始化大小是多少?为什么是这么多?为什么大小都是2的幂?

  答:

问七:HashMap的扩容方式?负载因子是多少?为什么这么多?

  答:

问八:HashMap的主要参数有哪些?

  答:

问十:HashMap的计算规则?

  答:

问十一:

  答:

数据结构之HashMap的更多相关文章

  1. 转发 java数据结构之hashMap详解

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  2. java数据结构之hashMap

    初学JAVA的时候,就记得有句话两个对象的hashCode相同,不一定equal,但是两个对象equal,hashCode一定相同,当时一直不理解是什么意思,最近在极客时间上学习了课程<数据结构 ...

  3. 程序员必须知道的数据结构:HashMap 与 LinkedHashMap

    为什么要说 HashMap 与 LinkedHashMap?第一:这两种数据结构是 Java Coder 中经常使用的数据结构.第二:这两种结构是最合适的能说明链表与数组的结构关系.在开始之前首先必须 ...

  4. LeetCode 哈希表 380. 常数时间插入、删除和获取随机元素(设计数据结构 List HashMap底层 时间复杂度)

    比起之前那些问计数哈希表的题目,这道题好像更接近哈希表的底层机制. java中hashmap的实现是通过List<Node>,即链表的list,如果链表过长则换为红黑树,如果容量不足(装填 ...

  5. 数据结构解析-HashMap

    概要 HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表+红黑树实现,主要的目的是提高查找效率. 如图所示: JDK版本 ...

  6. 深入解析Java对象的hashCode和hashCode在HashMap的底层数据结构的应用

    转自:http://kakajw.iteye.com/blog/935226 一.java对象的比较 等号(==): 对比对象实例的内存地址(也即对象实例的ID),来判断是否是同一对象实例:又可以说是 ...

  7. HashMap数据结构与实现原理解析(干货)

    HashMap 数据结构解析: HashMap内部使用hash表(本质是一个数组见图一) HashMap使用hash算法计算得到存放的索引位置,以此来加快查询速度,(比ArrayList还要快) 同样 ...

  8. Java中的数据结构-HashMap

    Java数据结构-HashMap 目录 Java数据结构-HashMap 1. HashMap 1.1 HashMap介绍 1.1.1 HashMap介绍 1.1.2 HashMap继承图 1.2 H ...

  9. HashMap和HashTable到底哪不同?

    HashMap和HashTable有什么不同?在面试和被面试的过程中,我问过也被问过这个问题,也见过了不少回答,今天决定写一写自己心目中的理想答案. 代码版本 JDK每一版本都在改进.本文讨论的Has ...

随机推荐

  1. jmeter json乱码

    0 环境 系统环境:win7 1 操作 1 找到jmeter.properties 找到jmeter下的bin目录jmeter.properties文件 例如apache-jmeter-\bin\jm ...

  2. [LC] 103. Binary Tree Zigzag Level Order Traversal

    Given a binary tree, return the zigzag level order traversal of its nodes' values. (ie, from left to ...

  3. 吴裕雄--天生自然 R语言开发学习:使用键盘、带分隔符的文本文件输入数据

    R可从键盘.文本文件.Microsoft Excel和Access.流行的统计软件.特殊格 式的文件.多种关系型数据库管理系统.专业数据库.网站和在线服务中导入数据. 使用键盘了.有两种常见的方式:用 ...

  4. HashMap、Hashtable、ConcurrentHashMap、ConcurrentSkipListMap对比及java并发包(java.util.concurrent)

    一.基础普及 接口(interface) 类(class) 继承类 实现的接口 Array √ Collection √ Set √ Collection List √ Collection Map ...

  5. centos 6.* 修改时间

    一.查看Centos的时区和时间 1.使用date命令查看Centos时区 [root@VM_centos ~]# date -R Mon, 26 Mar 2018 19:14:03 +0800 2. ...

  6. Spring_IOC

    我们都知道,如果要在不同的类中使用同一个对象一般我们我们都需要在每一个类中都去new一个新的对象,也有的人会为这个对象写一个工具类,无论哪种方法都需要我们自己去创建,不但繁琐,而且相当耗损资源,所以才 ...

  7. JStorm:任务调度

    前一篇文章 JStorm:概念与编程模型 介绍了JStorm的基本概念以及编程模型方面的知识,本篇主要介绍自己对JStorm的任务调度方面的认识,主要从三个方面介绍: 调度角色 调度方法 自定义调度 ...

  8. fiddler导出请求返回的响应数据

    或者右键 选择response导出

  9. Spring Boot 集成 Spring Security

    1.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...

  10. Python拾遗(2)

    包括Python中的常用数据类型. int 在64位平台上,int类型是64位整数: 从堆上按需申请名为PyIntBlcok的缓存区域存储整数对象 使用固定数组缓存[-5, 257]之间的小数字,只需 ...