JDK1.7HashMap源码分析
1.1首先HashMap中的Hash(哈希)是什么?
Hash也称散列,哈希,对应的英文都是Hash。基本原理就是把任意长度的输入通过Hash算法变成固定长度的输出,这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制就是哈希值。开发中经常使用的MD5和SHA都是属于Hash算法
MD5方法加密后的你好
在这个例子里 "你好" 是原始值, "7eca689f0d3389d9dea66ae112e5cfd7" 就是经过Hash算法得到的Hash值。整个Hash算法的过程就是把原始任意长度的值变成固定长度的值的过程。
1.2Hash算法的特点
1.Hash不可以反推导出原始的数据从上面的例子就可以看出经过Hash算法计算过的值和原始的数据是没有对应关系的。
2.输入数据的微小变化会得到不同的Hash值,相同的数据会获得相同的值,
这里就可以看到我们只加了一个数字1,但是整个获得的Hash值发生了非常大的变化。
3.Hash算法的执行效率要高效,就算是很长的文本信息也能够快速的计算出Hash值。
1.3Hash的用途
由于哈希的不可逆的特性使其能在以下的领域使用:
1. 密码:我们日常使用的各种电子密码本质上都是基于hash的,你不用担心支付宝的工作人员会把你的密码泄漏给第三方,因为你的登录密码是先经过 hash+各种复杂算法得出密文后 再存进支付宝的数据库里的。
2.文件完整性校验:通过对文件进行hash,得出一段hash值 ,这样文件内容以后被修改了,hash值就会变。
3.数字签名:数字签名技术是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。
此外还有区块链领域Hash算法也使用广泛。
1.4哈希表是什么?
说哈希表前要先说一下数组和链表
数组是在内存中的一片连续区域,同类型数据的集合,有索引,查询快,增删慢,不可扩容
链表是不连续的区域,每个节点放值和指向下一个节点的指针。查询慢,增删快。
而哈希表可以理解为数组和链表的结合,即一个一维数组,但是数组中的每一个元素是一个链表。
1.5为什么使用哈希表
首先我们有六个箱子(长度为6的数组)存储了数据
如果我们想查询这个数组里的 f 但是我们不知道 f 存储在哪个箱子里 只能从头开始查询这个操作叫做线性查询
于是从0号箱子先开始查询,这时候发现0号箱子里装的并不是 f 而是 a ,因此继续查找1号箱子 直到查找5号箱子的时候发现里面装的是 f
但是这样查找下来我们会发现数据量越大,查找消耗的时间就越长因此我们就得知了由于数据的查询比较耗时,所以这里不适合使用数组来存储数据。
所以这时候我们使用哈希表就可以解决这个问题,首先准备好数组,我们准备用五个箱子的数组来存储数据
如果我们想把one存入进去我们使用Hash计算one的值也就是字符one的哈希值比如得到的是4928,将得到的哈希值除以数组长度的5,求得的余数,这样的求余运算叫做mod运算
这里得到的结果是3。
于是我们将one的数据装进三号箱子里,重复前面的操作,将其他数据都存进数组中
但是当我们存入four键的哈希值经过计算后结果为1本应存入一号箱子中,但是此时一号箱子已经存储了three的数据,在哈希表中这种存储位置重复的情况叫冲突。
这时候我们可以使用链表在已有的数据后面继续存储新的数据
那么在哈希表中,我们可以利用哈希函数快速访问到数组中的目标。如果发生哈希冲突就使用链表进行存储,如果数组的空间太小,使用哈希表的时候就容易发生冲突,线性查找的使用频率会更高,反过来,如果数组的空间太大,就会出现很多空箱子,造成内存浪费。因此给数组设置合适的空间非常重要。
1.6哈希冲突的四种解决方法
第一种开放定址法(再散列法):
开发定址法就是一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
公式为:fi(key) = (f(key)+di) MOD m (di=1,2,3,……,m-1)
比如说我们有一个集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。我们用散列函数f(key) = key mod 12
当计算前几个数的时候是都的没有发生冲突的散列地址直接存入
可是当我们计算key = 37 时发现 f(37) = 1 ,此时就与25所在的位置冲突,于是我们应用上面的公式f(37) = (f(37) + 1) mod 12 = 2。于是将37存入下标为二的位置
2. 再哈希法:
当有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,. . . , 等哈希函数
计算地址,直至无冲突。虽然不易发生聚集,但是增加了计算时间。
3.链地址法:
链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
4.建立公共溢出区:
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
1.7HashMap源码JDK1.7
以上是定义的常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; 默认初始容量,为2的四次方 16。
static final int MAXIMUM_CAPACITY = 1 << 30; 最大容量为2的三十次方。
static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的负载因子(扩容因子) 0.75。
static final Entry<?,?>[] EMPTY_TABLE = {}; 一个空数组。
transient int size; 这个是键值对的个数。
构造方法
HashMap有三个构造方法,我们经常用到是第三个无参构造方法。
第一个构造方法需要传入自定义的负载因子与容器大小。
第二个构造方法也是要传入一个自定义的负载因子而容器的初始大小为16。
put方法
- 1 public V put(K key, V value) {
- 2 if (table == EMPTY_TABLE) { //首先判断内容是否是空的,tbale是一个默认为空的键值对数组。
- 3 inflateTable(threshold); //如果内容是空的那么就进行初始化。
- 4 }
- 5 if (key == null) //判断传入的键是否为空
- 6 return putForNullKey(value); //如果为空就调用这个方法
- 7 int hash = hash(key); //如果键不为空就计算键的Hash值
- 8 int i = indexFor(hash, table.length); //根据计算后的Hash值和数组的长度计算索引,就是这个键要存入的位置。
- 9 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //循环遍历数组,数组上每个元素都是一个链表。
//这时我们已经获得索引就遍历索引上的链表,这样做是为了检查传进来的key是否存在在表里了- 10 Object k;
- 11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //判断如果传进来的key和链表上的元素 Hash,地址,内容都一样那么说明已经存在在表里了
- 12 V oldValue = e.value; //那么就保持key不变,更新value 并且返回旧的value
- 13 e.value = value;
- 14 e.recordAccess(this);
- 15 return oldValue;
- 16 }
- 17 }
- 18
- 19 modCount++;//被修改次数加一,这个值主要是为了线程安全考虑,和本方法的业务无关
- 20 addEntry(hash, key, value, i);//创建一个节点到数组上
- 21 return null;
- 22 }
初始方法 inflateTable(threshold) 做了些什么
- private void inflateTable(int toSize) {
- // Find a power of 2 >= toSize
- int capacity = roundUpToPowerOf2(toSize); //初始化时无论存入的初始容量值是多少都会向上取整变为2的幂。也就是我们看到有一个构造方法可以让用户去自定义容量大小
但是传进来的容量也会向上取整变成2的密。在这里可以看见他做了一个-1的操作是为了防止把正确的容量翻一倍比如你传进来的是15,那么向上取整为16.如果传进来的是16,向上取整就成了32。
所以在任何数取整时先-1。
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //计算扩容值- table = new Entry[capacity]; //初始化
- initHashSeedAsNeeded(capacity);
- }
如果key为null的时候做了什么
- private V putForNullKey(V value) {
- for (Entry<K,V> e = table[0]; e != null; e = e.next) {//首先循环遍历如果发现链表里还有null,那么就替换value,也就是说key可以为空,但是只能有一个。
- if (e.key == null) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(0, null, value, 0);//如果第一次插入的key为null的元素就直接保存到索引为0的位置上
- return null;
- }
在JDK1.7中插入数据采用的是头插法,新来的元素会加在链表的开头,类似于栈。因为开发者认为后加的元素可能被用到的几率更大,所以头插法可以快速查询。
当然这也带来了安全隐患,就是在多线程环境下,可能会出现死循环。
for循环的数组如果数据很大.执行到Entry<K,V> next = e.next; 之后,既next的指针已经被记录,并在最后指向了下一次要循环的e.
突然.被阻塞.另一个线程过来,对同一个e对象进行了赋值,此时进行1到7步的操作,链表的数据都被插入到了新的索引下标,并且头插法将数据由原来的顺序变为倒序,
而原始指向的e.next对象已经有原来的头节点变为了尾节点,此时继续走1到7步.会产生原来的头结点和尾节点相互next指向.既发生死锁(自旋操作)
有可能在下次get的时候出现死循环的问题。
因为没有加锁,所以在多个线程并发的时候,不能保证数据是安全的.就是我put进去的一个值,不能保证我get的时候还是原来的值。
为了解决这个问题,jdk8采用了尾插法。不过HashMap本身就不是线程安全的,所以不建议在多线程下用。
小结:
1.jdk1.7采用数组+单向链表的数据结构进行存储数据,好处是便于解决hash冲突,坏处是可能增大get元素的遍历成本。
2.插入链表数据时采用头插法来进行插入。
3.hash寻址计算索引下表是通过hashCode计算出来的hash值跟集合的length-1进行与操作算出来的。
4.put时没有加锁,会导致get时不一定是自己添加的值。
以上就是我对JDK1.7HashMap的源码分析了,JDK1.8我会新写一篇文章。。。。。。。。
JDK1.7HashMap源码分析的更多相关文章
- 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)
一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...
- 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)
一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...
- 【集合框架】JDK1.8源码分析之HashMap(一) 转载
[集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...
- 【集合框架】JDK1.8源码分析之ArrayList详解(一)
[集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...
- 集合之TreeSet(含JDK1.8源码分析)
一.前言 前面分析了Set接口下的hashSet和linkedHashSet,下面接着来看treeSet,treeSet的底层实现是基于treeMap的. 四个关注点在treeSet上的答案 二.tr ...
- 集合之LinkedHashSet(含JDK1.8源码分析)
一.前言 上篇已经分析了Set接口下HashSet,我们发现其操作都是基于hashMap的,接下来看LinkedHashSet,其底层实现都是基于linkedHashMap的. 二.linkedHas ...
- 集合之HashSet(含JDK1.8源码分析)
一.前言 我们已经分析了List接口下的ArrayList和LinkedList,以及Map接口下的HashMap.LinkedHashMap.TreeMap,接下来看的是Set接口下HashSet和 ...
- 【1】【JUC】JDK1.8源码分析之ArrayBlockingQueue,LinkedBlockingQueue
概要: ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞 注意这两个Condition即ReentrantLock的Co ...
- 【1】【JUC】JDK1.8源码分析之ReentrantLock
概要: ReentrantLock类内部总共存在Sync.NonfairSync.FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQ ...
随机推荐
- Jar 包下载以及 maven jar 包配置
学习内容: jar包下载是我们必须掌握的一个内容,不管是使用Maven项目还是其他项目,一般都需要引入外部的 jar 包 jar包下载 下载地址(打不开网址的直接百度搜索 maven reposito ...
- window nginx 简单搭建服务器访问静态资源
nginx命令: 启动: start nginx 停止:nginx -s stop || nginx -s quit 注:stop是快速停止nginx,可能并不保存相关信息:quit是完整有序的停止 ...
- Docker-操作容器1
->点击该链接:Linux(Centos7)安装Docker<- 前言 步骤: 软件镜像->运行镜像->产生一个容器 这就类似于我们在pc端下载微信时需要启动wechat.ex ...
- Win下GCC报错 error: ‘for’ loop initial declarations are only allowed in C99 mode
##报错## 用GCC编译for循环会出现以下错误 error: 'for' loop initial declarations are only allowed in C99 mode 如图所示: ...
- Abp 实现通过手机号注册用户
前言 Abp 的 Identity 模块,实现了用户的管理,但是对于国内来讲,很多场景不能很好适配.比如:通过手机号进行注册的场景. Abp vnext Identity 以及 asp.net cor ...
- 使用Lua 脚本实现redis 分布式锁,报错:ERR Error running script (call to f_8ea1e266485534d17ddba5af05c1b61273c30467): @user_script:10: @user_script: 10: Lua redis() command arguments must be strings or integers .
在使用SpringBoot开发时,使用RedisTemplate执行 redisTemplate.execute(lockScript, redisList); 发现报错: ERR Error run ...
- Java语言学习day13--7月14日
今日内容介绍1.循环练习2.数组方法练习 ###01奇数求和练习 * A: 奇数求和练习 * a: 题目分析 * 为了记录累加和的值,我们需要定义一个存储累加和的变量 * 我们要获取到1-100范围内 ...
- Go 语言 结构体链表
@ 目录 1. 什么是链表 2. 单项链表的基本操作 3. 使用 struct 定义单链表 4. 尾部添加节点 5. 头部插入节点 6. 指定节点后添加新节点 7. 删除节点 1. 什么是链表 链表是 ...
- 【论文笔记】A Survey on Federated Learning: The Journey From Centralized to Distributed On-Site Learning and Beyond(综述)
A Survey on Federated Learning: The Journey From Centralized to Distributed On-Site Learning and Bey ...
- PowerDotNet平台化软件架构设计与实现系列(13):应用监控平台
本文再写一篇和具体业务逻辑几乎无关的公共服务应用监控平台.PowerDotNet自研的应用监控平台系统,是服务治理的重要拼图,和服务治理平台配合使用效果更好. 监控开源产品非常丰富,站在巨人的肩膀上, ...