上一篇文章让是我对整个java集合的基础认识,让我能够使用集合,下面我将对其中几个重要集合的(ArrayList,LinkList,HashMap)源码进行阅读。

一、HashMap是什么?

弄清楚这点!!我觉得这是最重要的一点。
官方的解释是:

HashMap是基于哈希表的实现的Map接口。此实现提供了所有可选的Map操作,并允许null的值和null键。 ( HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null)。这个类不能保证Map的顺序; 特别是,它不能保证订单在一段时间内保持不变。

为什么要用Hash

我了解了下为什么要用hashcode计算存入的键:
因为我们存入的键不能重复,那么我们怎么判断重复了,当然是用hashcode()方法把键转成hash码进行比较咯。为什么不直接比较呢,我的理解是,因为直接比较就跟肉眼去看两个人是否是双胞胎,而用hashcode计算后的哈希码比较就相当于把他们的DNA进行比较。
但别人是这样解释的:

也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。

哈希是什么

把任意长度的输入(输入叫做预映射,知道就行),通过一种函数(hashCode() 方法),变换成固定长度的输出,该输出就是哈希值(hashCode),这种函数就叫做哈希函数,而计算哈希值的过程就叫做哈希。哈希的主要应用是哈希表和分布式缓存。

哈希冲突又是什么:

哈希表选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是哈希冲突(哈希碰撞)。
解决哈希冲突 有两种方法,拉链法(链接法)和开放定址法。拉链法就是:将键值对对象封装为一个node结点(数组中的一个值),新增了next指向,这样就可以将碰撞的结点链接成一条单链表,保存在该地址(数组位置)中。

HashMap中并不是直接用的hashcode产生的哈希码,而是进行了一些位计算,但并未改变结果,至于为什么这么做 请参考这篇博客:HashMap中的hash算法中的几个疑问 总之就是为了提高性能

HashMap中的hash函数的源码如下:

static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位异或// >>>:无符号右移,忽略符号位,空位都以0补齐return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

二、HashMap的存储结构:

网上这张图片即表示了运用了拉链法的HashMap的存储结构(也叫桶数组):
当存在相同hashcode的key的时候,把他插入后面的链表,使用的是头插法(因为HashMap的发明者认为,后插入的key被查找的可能性更大)。
HashMap的这种特殊存储结构在获取指定元素前需要把key经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率极高。
图中数组的索引= HashCode(Key)%length //length为HashMap的长度

均匀分布提高效率?

均匀分布其实是为了减少hash碰撞,因为减少了碰撞,效率才会提升。
哈希表长度越长,空间成本越大,哈希函数计算结果越分散均匀。哈希函数计算结果越分散均匀,哈希碰撞的概率就越小,map的存取效率(时间复杂度)就会越高。 因此我们的HashMap设计者进行了堪称完美的设计来提高效率。

实现均匀分布:
那么如何实现一个尽量均匀分布 的Hash函数呢?当然是把计算的哈希值当做数组索引进行存储在数组中咯,但是 Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,内存肯定放不下,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标,所以开始的公式是:HashCode(Key)% length后来经过研究发现取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方?

至于为什么能这样能均匀分布请参考:漫画讲解HashMap

看到一位大佬对使用HashMap的特点解释如下:下面我也将通过源码逐个讲解他们具体是如何通过代码实现的。

HashMap的出现是为了实现一种快速的查找并且插入、删除性能都不错的一种K/V(key/value)数据结构:

  • 为了实现快速查找,HashMap 选择了数组而不是链表。以利用数组的索引实现 O(1) 复杂度的查找效率。
  • 为了利用索引查找,HashMap引入 Hash 算法, 将 key 映射成数组下标: key -> Index。 引入 Hash 算法又导致了 Hash 冲突。
  • 为了解决Hash 冲突,HashMap 采用链地址法,在冲突位置转为使用链表存储。 链表存储过多的节点又导致了在链表上节点的查找性能的恶化。
  • 为了优化查找性能,HashMap 在链表长度超过 8 之后转而将链表转变成红黑树,以将 O(n) 复杂度的查找效率提升至 O(log n)。

三、HashMap.java

3.1、属性解释

下面是这个类中的属性解释:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列号private static final long serialVersionUID = 362498820763181265L;    // 默认的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD = 6;// 桶中结构转化为红黑树对应的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组,总是2的幂次倍transient Node<k,v>[] table; // 存放具体元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transient int size;// 每次扩容和更改map结构的计数器 例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化transient int modCount;   //临界值(最大node结点(键值对)容量)当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 填充因子final float loadFactor;
}

两个重点属性:

loadFactor加载因子:

loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

threshold:

threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

3.2、HashMap的扩容

扩容分为两种:
①创建时如果不指定容量初始值,HashMap默认的初始化大小为16,之后每次扩充,容量变为原来的2倍。(区别一下Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1)
②创建时如果给定了容量初始值, HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面详讲)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。(Hashtable 会直接使用你给定的大小)

3.3、解决hash冲突

我们知道解决hash冲突的原理就是,当发生碰撞时,就把相同hash值的 K-V对 组成链表。当链表长度大于阈值(即属性TREEIFY_THRESHOLD 默认为8)时,将链表转化为红黑树,以减少搜索时间。

/**这个方法即保证了 HashMap 总是使用2的幂作为哈希表的大小。* Returns a power of two size for the given target capacity.* 返回给定容量的两倍大小*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

3.4、构造方法

3.5、存数据-----put方法

3.6、取数据-----get方法

其他方法:

get,put方法参考

HashMap源码阅读启读相关推荐

  1. HashMap源码阅读笔记

    HashMap是Java编程中常用的集合框架之一. 利用idea得到的类的继承关系图可以发现,HashMap继承了抽象类AbstractMap,并实现了Map接口(对于Serializable和Clo ...

  2. HashMap 源码阅读

    前言 之前读过一些类的源码,近来发现都忘了,再读一遍整理记录一下.这次读的是 JDK 11 的代码,贴上来的源码会去掉大部分的注释, 也会加上一些自己的理解. Map 接口 这里提一下 Map 接口与 ...

  3. HashMap jdk1.7源码阅读与解析

    转载自  HashMap源码阅读与解析 一.导入语 HashMap是我们最常见也是最长使用的数据结构之一,它的功能强大.用处广泛.而且也是面试常见的考查知识点.常见问题可能有HashMap存储结构是什 ...

  4. jdk源码阅读-HashMap

    前置阅读: jdk源码阅读-Map : http://www.cnblogs.com/ccode/p/4645683.html 在前置阅读的文章里,已经提到HashMap是基于Hash表实现的,所以在 ...

  5. HashMap 源码详细分析(JDK1.8)

    1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...

  6. Java8 LinkedHashMap 源码阅读

    如果你对 HashMap 的源码有了解的话,只需要一图就能知道 LinkedHashMap 的原理了,但是具体的实现细节还是需要去读一下源码. 一.LinkedHashMap 简介 1.1 继承结构 ...

  7. MyBatis 源码阅读 -- 核心操作篇

    核心操作包是 MyBatis 进行数据库查询和对象关系映射等工作的包.该包中的类能完成参数解析.数据库查询.结果映射等主要功能.在主要功能的执行过程中还会涉及缓存.懒加载.鉴别器处理.主键自增.插件支 ...

  8. 走过的路-java源码阅读之路

    源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 一.人生三种境界: 1.昨夜西风凋碧树,独上高楼望尽天涯路.           2.衣带渐宽终不悔,为伊消得人憔悴.           ...

  9. 遍历HashMap源码——红黑树原理、HashMap红黑树实现与反树型化(三)

    本章将是HashMap源码的最后一章,将介绍红黑树及其实现,HashMap的remove方法与反树型化.长文预警~~ 遍历HashMap源码--红黑树原理.HashMap红黑树实现与反树型化 什么是红 ...

最新文章

  1. 计算机组成原理-第3章-3.1
  2. 人工智能浪潮下的语音交互——VUI设计(基础篇)
  3. python适合零基础学习吗-零基础,经济学专业,适合自学Python吗?
  4. 解决Win10下_findnext()异常
  5. GDI绘制时钟效果,与系统时间保持同步,基于Winform
  6. Qt入门之基础篇 ( 一 ) :Qt4及Qt5的下载与安装
  7. Linux-Ubuntu中使用apt进行软件的安装与卸载
  8. 通过exp命令对Oracle数据库进行备份操作(提供两种情况的备份:备份本地,备份远程的数据库)
  9. Access 时间比较错误
  10. python 元类的call总结_Python 类与元类的深度挖掘 I【经验】
  11. 外显子和基因组基本概念(一)
  12. Java中循环删除list中元素的方法总结(总结)
  13. html中立体丝带菜单,使用CSS3实现绚丽的飘带样式菜单方法介绍
  14. Jackson Annotation Examples
  15. 移动开发语言Swift
  16. android looper介绍
  17. WiFi 四次握手Omnipeek抓包
  18. Ubuntu过去十年的10个关键时刻
  19. 王二 设计模式读书笔记
  20. 用PS制作墙壁上的时尚立体文字图案

热门文章

  1. vscode中setting.json配置详解
  2. busybox的实现原理分析(C语言实现简易版的busybox)
  3. jsoncpp在vs2012下的环境搭建(C++)
  4. mysql查询是第几条记录_MySQL查询第几行到第几行记录
  5. Transformer拿下CV顶会大奖,微软亚研获ICCV 2021最佳论文
  6. Excel切片器的使用
  7. excel切片器_听说你还不会用切片器?比筛选好用100倍,小白也能学会
  8. Unity后处理效果之边角压暗
  9. 2020年计算机专业研究生考试时间,2020计算机考研考试时间及考试内容
  10. 《牧羊少年奇幻之旅》读后感