以下源码均为jdk1.7

HashMap概述

HashMap是基于哈希表的Map接口的非同步实现. 提供所有可选的映射操作, 并允许使用null值和null健. 此类不保证映射的顺序.

需要注意的是: HashMap不是同步的.

哈希表

哈希表定义: 哈希表是一种根据关键码去寻找值的数据映射结构, 该结构通过把关键码映射的位置去寻找存放值的地方.

举个例子, 最典型的例子就是字典, 如果想要在字典中查找"按"字, 通常会根据拼音 an 去查找拼音索引(当然也可以是偏旁索引), 然后找到 ti 在字典中的位置, 得到第一个拼音为 an 的字 "安". 这个过程就是键码映射, 即 通过 key 查找 f(key). 其中 key为关键字, f()是哈希函数, 哈希函数的结果就是哈希值.

哈希冲突: 那么问题来了, 我们要查找的是"按",而不是"安", 但他们的拼音都是一样的. 通过关键字 an "按"和"安"可以映射到一样的字典页码4的位置, 这就是哈希冲突(也叫哈希碰撞), 在公式上表达就是 key1 != key2, 但f(key1)=f(key2).

key 为值, f(key)计算得出数组中存储地址, 这样就会出现两个元素的地址相同的情况. 这时, 哈希函数的设计就至关重要了, 好的哈希函数会尽可能的保证 计算简单和散列地址分布均匀, 但是, 数组是一个连续的固定长度的内存空间, 再好的哈希函数也不能保证得到的存储地址绝不发生冲突.

哈希冲突的解决方案有多种: 开放定址法(发生冲突, 寻找下一个), 再散列函数法, 链地址法.

HashMap就是采用了链地址发, 也就是 数组+链表 的方式.

HashMap的实现原理

最基本的数据结构有两种: 数组和指针, HashMap就是通过这两个数据结构实现的, 是数组和链表的结合体.

从图中可以看出, HashMap底层是一个数组结构, 数组中的每一项是一个链表. 当新建HashMap时, 会初始化一个数组.

HashMap的主干是一个Entry数组.

Entry是一个静态内部类, 包含 key-value.

HashMap存储的整体结构如下:

简单说, HashMap有数组+链表组成, 数组是HashMap的主体, 链表是为了解决哈希冲突而存在的, 如果定位到数组位置不含链表(当前entry的next指向null), 那么对于查找,添加等操作很快, 仅需一次寻址即可; 如果定位到的数组包含链表, 那么添加操作就要遍历链表, 然后通过key的equals方法进行逐一对比, 存在即覆盖, 不存在则新增, 而查找操作也需遍历链表.

所以, 性能考虑, HashMap中的链表出现越少, 性能越好.

HasmMap几个重要的字段:

HashMap的构造函数:

从上面代码中可以看出, 在常规构造器中, 没有为数组 table 分配内存空间(有个参数为map的构造器除外), 而是在执行 put操作时才真正构建table数组

再来看 inflateTable()方法源码:

重量级角色, 哈希函数出场:

indexFor()函数实现如下:

h&(length - 1)保证获取的index一定在数组的范围内, 例如: 容量为16, length-1=15, h=18, 进行计算为:

得出index=2.

故而, 最终存储位置的确定为如下流程:

最后看下 addEntry 的实现:

通过 addEntry 的代码可以看出, 当发生哈希冲突并且size大于阈值时, 需要进行数组扩容, 扩容时, 需要新建一个长度为之前2倍的新数组, 最后将当前的Entry数组中元素全部传过去, 扩容后的新数组长度为之前的2倍, 所以扩容相对来说是一个耗资源的操作.

下面看get方法就简单得多了:

然后是getEntry()源码:

可以看出, get方法的实现相当简单, 流程为: key(hashcode)-->hash-->indexFor-->最终索引位置, 找到对应位置table[i], 在查看是否有链表, 遍历链表, 通过key的equals方法比对查找对应的记录.

在getEntry方法中, 定位到数组位置之后遍历链表的时候, e.hash==hash这个判断是否有必要. 试想如下场景, 如果传入的key对象重写了equals方法却没有重写hashCode, 而恰巧此对象定位到这个数组位置, 如果仅仅用equals判断可能是相等的, 但其hashCode和当前对象不一致, 这种情况, 根据Object的hashCode的约定, 不能返回当前对象, 而应该返回null.

重写equals方法要同时重写hashCode方法

为什么重写equals时也要同时重写hashCode? 下面举个小例子:

实际输出结果:

结果: null

现在我们已经对HashMap的原理有了一定了解, 这个结果就不难理解了. 尽管我们在进行get和put操作的时候, 使用的key从逻辑上讲是等值的, 但由于没有重写hashCode方法, 在进行put操作时: key(hashcod1)-->hash-->indexFor-->最终索引位置; 而通过key去除value时: key(hashcode2)-->hash-->indexFor-->最终索引位置, 由于hashcode1和hashcode2不相等, 最终得出的数组索引页不一样而返回null(也可能碰巧定位到了一个数组位置, 但是也会判断其entry的hash值是否相等, 上面get方法中有提到)

所以, 在重写equals方法时, 必须注意重写hashCode方法, 同时还要保证equals判断相等的两个对象, 调用hashCode方法要返回同样的整数值, 而equals判断不相等的两个对象, 其hashCode可以相同, 只是会发生哈希冲突, 应该尽量避免.

HashMap的遍历

总结

HashMap底层将key-value当成一个整体处理, 这个整体就是Entry对象. HashMap底层采用一个Entry[]数组来保存所有的key-value对, 当需要存储一个Entry对象时, 会根据hash算法来决定其在数组中的位置, 再根据equals方法决定其在该数组位置上的链表中的存储位置; 当需要取出一个Entry时, 也会根据hash算法找到其在数组中的存储位置, 再根据equals方法从该位置上的链表中取出该Entry.

Java集合之HashMap源码分析的更多相关文章

  1. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  2. 死磕 java集合之HashMap源码分析

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改 ...

  3. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  4. 【转】Java集合:HashMap源码剖析

    Java集合:HashMap源码剖析   一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调 ...

  5. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  6. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  7. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  8. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  9. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

随机推荐

  1. 2018年2月19日我的java学习

    2019/2/18 星期一今天学习了Java 中的面向对象思想主要学习了类 构造器等在学习修饰属性的过程中,有4点必须牢记前提是理解类的各种关系 类中有5种关系 本身 同包类 同包继承子类 不同包继承 ...

  2. 9.3AspectJ

    1.1 介绍 AspectJ是一个基于Java语言的AOP框架 Spring2.0以后新增了对AspectJ切点表达式支持 @AspectJ 是AspectJ1.5新增功能,通过JDK5注解技术,允许 ...

  3. Delphi fmx控件在手机滑动与单击的问题

    Delphi fmx控件在手机滑动与单击的问题 (2016-03-08 10:52:00) 转载▼ 标签: it delphi 分类: Delphi10 众所周知,fmx制作的app,对于象TEdit ...

  4. JS跨域调用之document.domain--相同基础域名页面之间的调用

    利用document.domain 实现跨域: 前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域 Javascript出于对 ...

  5. 开源播放器 ijkplayer (三) :ijkplayer支持 https 编译流程

    主要是为了支持flv和m3u8,使用https播放视频的需求 ./init-android.sh ./init-android-openssl.sh // 增加https协议支持 cd android ...

  6. Yii2 三层设计模式:SQL Command、Query builder、Active Record(ORM)

    用Yii2也有一段时间了,发现Yii2 Framework对Database的操作有非常良好的结构和弹性. 接下来介绍三种数据库操作方式. SQL Command Level: // Get DB c ...

  7. Javascript高级编程学习笔记(5)—— JS操作符

    话不多说,开始今天的码字之旅. 突然有种日更小说的感觉,emm... 操作符 ECMAScript(JS核心)描述了一组用于操作数据值的操作符,也包括算术操作符等等 而JS中这些操作符最鲜明的特点就是 ...

  8. 学生管理系统(springMVC)

    <Java Web编程>课程设计                                                                               ...

  9. Spring Boot 配置随机数技巧

    Spring Boot支持在系统加载的时候配置随机数. 添加config/random.properties文件,添加以下内容: #随机32位MD5字符串 user.random.secret=${r ...

  10. javascript实现二分法

    js 实现数组查找二分法 二分法实现原理:二分查找可以解决已经排好序数组的查找问题:只要数组中包含target(即要查找的值),那么通过不断缩小包含target数组的范围,最终就可以找到它. 其算法流 ...