Java源码学习:HashMap实现原理
AbstractMap
HashMap继承制AbstractMap,很多通用的方法,比如size()、isEmpty(),都已经在这里实现了。来看一个比较简单的方法,get方法:
public V get(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return e.getValue();
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return e.getValue();
}
}
return null;
}
单从这里看看不到Map的搜索策略,这里显示的仅仅就是遍历全部元素挨个测试是否匹配。
remove方法中先匹配到元素,然后利用迭代器Iterator的remove方法将元素从记录中删除。
public V remove(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
Entry<K,V> correctEntry = null;
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
transient和volatile
终于看到存储key和value的地方了,这里马上出现了两个Java关键字,transient和volatile:
transient volatile Set<K> keySet = null;
transient volatile Collection<V> values = null;
transient关键字的意思是说改字段不会被持久化和反持久化,这个会在对象序列化到文件时用到。参考这里。
volatile就比较复杂一点儿了,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
有点儿线程安全的意思,就是说一个变量被另外一个线程修改了,其他在使用这个变量的线程也会知道。
文章中所举例子:
//线程1
boolean stop = false;
while(!stop){
doSomething();
} //线程2
stop = true
以上代码是有可能死循环的。
接下来初始化Entry时,如果用一个Map去初始化另外一个Map,那么这个Map的初始大小将为原先Map的2倍:
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
/**
* Inflates the table.
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
Integer.highestOneBit(num)
拿到原先Map的size之后初始化一个新的Entry数组,这个数组的的size增加到原先Map的2倍。方法Integer.highestOneBit(num)的作用是得到比num还大的但是是2的指数倍的数。这些数其实就是2,4,8,16,32,64,128,256,512
可以看出JDK源码中很多地方对于*2这种操作都不是直接乘以2,而是采用向左位移一位,比如:
(number - 1) << 1
HashMap数据结构

插入元素
1、key为null的Entry存放在数组Entry[]的第一位的Entry链表中,即Entry[0],仔细看看,Map.put()方法其实是有返回值的,这个返回值就是被替换掉的Value(如果存在的话)。
2、key不为空,通过Hash散列之后存入数组不同位置的链表中。散列中用到了按位与(&)运算:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
如何进行Hash散列
按位与运算规则如下:
0 & 0 = 1
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
也就是只有同时两个都为1时才等于1。这里要求数组的长度必须是2指数倍是有原因的。比如length = 256(2的8次方),那么它换算成二进制就是1后面8个0:
256 - 1 换算成二进制刚好是7个1:
用这个数与任意的数N进行按位与运算的效果是:保留N的后7位:
&
-------------------------------
后面这7位就像亮着的几盏灯,亮几盏就能截取多少位。正是这样实现了数据的Hash散列。
从上面的代码可以看出,Hash散列时仅用到了了Object.hashCode()的后几位,如果n - 1 = 15即0x1111,那么发送Hash冲突的可能性会非常大,为了解决这个问题,可以理解为需要在原先的Object.hashClde()基础之上做一些混淆,即使整个原始HashCode都会影响最终的散列。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这里通过高位与低位(向右位移的距离不一样)的异或运算进行“混淆”。
总结
HashMap的实现原理?
通过元素的哈希码来做映射,将数据散列到一个数组中,如果发生了哈希冲突则将冲突的元素形成一个链表进行存储。Java8中进行了优化,冲突的元素多到一定程度时,将改链表为红黑树,这样有效提高了高冲突时的性能;
HashMap需要注意些什么?
注意两个参数:
容量(Capacity):容器也可以叫做数组的初始大小,如果元素增加到一定程度(也就是负载因子),就会将容量翻倍。
负载因子(Load factor):默认负载因子是0.75,也就是当元素超过四分之三的时候会增加数组的大小。
需要注意的是,如果你想要HashMap遍历得更快,应该把容量设计得小点儿、负载因子设计大点儿,这样其实是让HashMap的数组存储地更密集些,能提高遍历速度。
Java源码学习:HashMap实现原理的更多相关文章
- JDK1.8源码学习-HashMap
JDK1.8源码学习-HashMap 目录 一.HashMap简介 HashMap 主要用来存放键值对,它是基于哈希表的Map接口实现的,是常用的Java集合之一. 我们都知道在JDK1.8 之前 的 ...
- Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题
Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题 相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题 ...
- 在IDEA中搭建Java源码学习环境并上传到GitHub上
打开IDEA新建一个项目 创建一个最简单的Java项目即可 在项目命名填写该项目的名称,我这里写的项目名为Java_Source_Study 点击Finished,然后在项目的src目录下新建源码文件 ...
- Java 源码学习线路————_先JDK工具包集合_再core包,也就是String、StringBuffer等_Java IO类库
http://www.iteye.com/topic/1113732 原则网址 Java源码初接触 如果你进行过一年左右的开发,喜欢用eclipse的debug功能.好了,你现在就有阅读源码的技术基础 ...
- Java 源码学习系列(三)——Integer
Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的字段. 此外,该类提供了多个方法,能在 int 类型和 String 类型之间互相转换,还 ...
- 以太坊 layer2: optimism 源码学习(二) 提现原理
作者:林冠宏 / 指尖下的幽灵.转载者,请: 务必标明出处. 掘金:https://juejin.im/user/1785262612681997 博客:http://www.cnblogs.com/ ...
- Java源码阅读HashMap
1类签名与注释 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cl ...
- JAVA源码分析-HashMap源码分析(一)
一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...
- Java源码学习 -- java.lang.StringBuilder,java.lang.StringBuffer,java.lang.AbstractStringBuilder
一直以来,都是看到网上说“ StringBuilder是线程不安全的,但运行效率高:StringBuffer 是线程安全的,但运行效率低”,然后默默记住:一个是线程安全.一个线程不安全,但对内在原因并 ...
随机推荐
- 正则表达式-linux基础
通配符 通赔符是模糊匹配的基础,因为通赔符机制的存在使得我们在查找文件的时候非常方便 * :匹配任意长度的任意字符,也可以一个都没有 . :匹配任意单个字符,必须有一个 [ ] ...
- jsp的C标签一般使用方法以及js接收servlet中的对象及对象数字
jsp的C标签一般使用方法以及js接收servlet中的对象及对象数组 由于现流行的javaWeb框架提倡前后端分离,比如在SpringMvc中已经很少写servlet的一些东西:目前 前端jsp中大 ...
- [读书笔记] 二、条件注解@Conditional,组合注解,元注解
一.条件注解@Conditional,组合注解,元注解 1. @Conditional:满足特定条件创建一个Bean,SpringBoot就是利用这个特性进行自动配置的. 例子: 首先,两个Condi ...
- 使用jmeter进行批量数据创建
背景: 测试环境需要创建大量的测试数据,进行功能和性能的测试 测试数据创建接口是HTTP请求方式 测试数据要求不同类型的数据,要有字段进行关联,且单据的编号在DB中唯一不可重复,此外测试数据的时间类参 ...
- macaca 测试web(3)
上回书说到 macaca 测试web(2) 使用ddt做参数驱动化, 有些人会说,你好low,我说怎么low呢,他说你看看你的脚本就放在一个文件里,对于我们小白来说,这看起来很乱啊,能不能给我拆分, ...
- Day-6: 函数式编程
函数式编程就是封装成一个个函数,一次调用来完成复杂任务. 函数式编程的一个特点是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数! 高阶函数 高阶函数就是将函数的变量名作为参数传入,内部再对 ...
- 初学者入门web前端:C#基础知识:函数
入行前端对函数的掌握程度有可能直接影响以后工作的效率,使用函数可以高效的编写编码,节省时间,所以我整理了C#中最基础的函数知识点,虽然我在学习中 遇到很多问题,但是只要能够解决这些问题,都是好的. 一 ...
- JS的this总结(上)-call()和apply()
JS的this总结(上)-call()和apply() 相信很多人在学习JavaScript的过程中,都会了解到this,而大部分人都会特意去网络上搜一下相关资料,大部分的文章都有这么一句话: t ...
- JUnit之TestCase和TestSuite详解
首先介绍下TestCase以及TestSuite. TestCase:字面意思,测试用例.为一个或多个方法提供测试方法.一般是一个test TestSuite:测试集合,即一组测试.一个t ...
- 团队作业4——第一次项目冲刺 SeCOnd DaY
项目冲刺--Double Kill 喂喂喂,你好你好,听得见吗?这里是天霸动霸.tua广播站,我是主播小学生¥-¥ 第一次敏捷冲刺平稳的度过了第一天,第一天的任务大家也圆满完成啦[拍手庆祝],那么今天 ...