在Java语言中使用的最多的数据结构大概右两种,第一种是数组,比如Array,ArrayList,第二种链表,比如ArrayLinkedList,基于数组的数据结构特点是查找速度很快,时间复杂度为 O(1),但是删除的速度比较慢,因为每次删除元素的时候需要把后面的所有的元素都要相应的往前移动一位,最坏的情况删除第一个元素,时间复杂度为O(n)。基于链表实现的数据结构的特点是删除的速度比较快,但是查找的速度比较慢,每次查找数据的时候都需要从链表头部开始往下遍历,链表查找最坏时间是O(n)。HashMap 就整合和了数组和链表的有点而设计出来的,它的查找速度为 O(1) + O(a),a为链表长度,事实上hashMap的hash算法能够很好的避免了在插入数据的碰撞问题,所以链表的长度基本不会很长,所以hashMap的查找速度还是很快的。一般地,我们平衡一种结构的性能是看平均时间复杂度的,在 jdk1.8以前hashMap在最糟糕的情况下查找的时间复杂度为 O(1) +O(n) ,n 为数据的大小。在jdk1.8时sun公司对hashMap进行了优化,hashMap的存储结构由原来的数组+链接的结构改成 数组+链表+红黑树的形式。时间复杂度由O(1) + O(n) 降为 O(1) + O(logn)。下面的源码都是基于jdk1.8的。

HashMap中几个重要的参数:

  1、threshold : 数组的大小,默认长度为16,可以在构造函数中指定初始化大小,但是必须是2的n次方,具体原因在下面将会说到。注意:该值是指数组的大小,并不是指HashMap中已经存放了的数据量,存放的数据的大小总是小于等于 threshold * loadFactor。

  2、loadFactor: 负载因子,默认值为0.75。当HashMap中存储的数据大于阈值(threshold * loadFactor)时,threshold会进行翻倍,执行resize方法,对原数组中所有的元素进行一次重新hash计算,根据hash计算得出的下表放在新的数组中。负载因子的设计是为了减少在put操作时发生的碰撞,因为当我们put的数据越来越多的时候,数组中空的位置也会越来越少,那么发生碰撞的概率也随之增大,碰撞的次数越多对性能由一定的影响。一般地我们不需要对这个值进行设置,使用默认值就可以了。

  3、TREEIFY_THRESHOLD:转换红黑树的阈值,默认值为8。即当数组中链表的长度达到这个值之后,链表就是转换成红黑树,以提高性能。

  4、UNTREEIFY_THRESHOLD:红黑树转链表的阈值,默认值为6。

HashMap结构

HashMap是以key-value的形式存储数组中,将数据存在Node节点中,每个Node节点存储了一个key,对应的value和指向下一个Node的指针。HashMap的结构为数组+链表(红黑树),链表为单向链表。结构如下:

或:

HashMap原理:

 HashMap在进行put(key,value)操的时候,我们看源码

/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 通过位与的方式来确定下标位置,判断当前下标位置是否为空,如果为空直接放入到该位置上
* 不为空则通过equals方法来寻找当前位置上面的元素,如果有相同的key,则将覆盖掉,如果没有则将node放置在对应
* 位置上面
*/
if ((p = tab[i = (n - 1) & hash]) == null)//直接放到数组中
tab[i] = newNode(hash, key, value, null);
else {//当前位置不为空
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//已存在相同的key的数据,将其覆盖
e = p;
else if (p instanceof TreeNode)//当前位置是红黑树,将Node节点放到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//为链表的情况
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表的长度超过转换红黑数的阈值,则将该链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//覆盖相同key的node
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//快速失败机制
if (++size > threshold)//每次插入数据都要判断一下当前存储的数据是否需要扩容
resize();
afterNodeInsertion(evict);
return null;
}

从上面的中HashMap在put数据的是时候,可以总结为一下一个步骤:

1、先判断当前的数组是否为空(即有没有被初始化过),为空的话则进行扩容操作;每次扩容的大小为2的n次方。

2、通过上面的步骤后数组就已经初始化好了,然后第二步数组长度与key的hash值进行与运算,得出该数据即将放进去的数组的位置,这里可以分为以下两种情况:

1)当前数组的位置没有数据时,直接将该数据放进数组中;

2)该位置有数据,先比较hash值再比较key值,如果两个都相等则将旧值替换掉;否则,从第一个节点开始一个遍历比较hash和key的值,有则替换,没有则放到最后。

3、当数据成功插入后,会进行一次判断当前的数组长度是否需要进行扩容。

当我们当HashMap中put数据的时候,首先会对传进来的key进行hash计算:

/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

jdk1.8开始hash的计算比之前的简单一些,就是对key的hashCode的高16位和低16位进行异或运算。这样做的目的是让key的HashCode 的高位也有计算参与运算,这样计算出来的hash值更加均匀,put数据时能够减少碰撞,提供性能。

  第二步根据key计算出来的值获取到对应的下标,这里并不是使用取模的方式来确定,因为取模的方式相对于位与运算来说性能更低下。下标的计算公式为:当前数组的长度减一 按位与 hash值,得到下标,比如当前数组长度为 16,hash值:54707624,则计算如下:(注意:位与的运算规则为,当两个数均为1时结果才为1,否则结果为0)

从上面的运算结果,可以得到一个规律,能够参与有效运算的位只有与数组长度减一的位的长度,比如 数组长度为16,那么16-1的二进制为 1111,那么不管key的hash值有多大,最终参与运算的只有后4位,根据位与运算规则,运算结果的最大值为 1111,转换成十进制后即数组的长度减一,最小值为 0000,十进制为0,即结果的范围为 0 ~ size - 1,这个取模的结果是一致的。又因为数组的长度总是2的n次方,对应的二进制 为 1,11,111 ,11111等等,这也是为什么每次扩容时都要扩大至原来的两倍的原因。那么,另外一个问题又来了,为什么一定要时2的n次方呢?其他的值可以吗?下面我们来做一个实验:

public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
System.out.print((i & 15) + " ");
}
} 运算结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13

当我们使用 2 的n次方-1来运算时,每个余数都有可能得到

public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
System.out.print((i & 13) + " ");
}
}
运算结果:
1 0 1 4 5 4 5 8 9 8 9 12 13 12 13 0 1 0 1 4 5 4 5 8 9 8 9 12 13

当我们使用 非2的n次方运算时,看运算结果可以看到,有些值是不可能得到的,这样数组的某些位置就永远为空,不仅造成空间的浪费,同时也会大大的提高碰撞的概率。根据位与运算规则,很容易想到其中的原因,首先将13转成二进制:1101,在位与运算时,那个 0 位永远不参与运算,如上面的结果一样,2,3,6等数值是没有的。当且仅当二进制数字全为1的时候,才有可能所有的位都能计算,得到的结果才会更加均匀。这个很容易理解,想一下就明白了的。

  第三步,根据生成的index去数组寻找位置,如果该位置为空直接将node放进去,如果不为空则调用equals方法判断key值是否一致,一致的话就替换成新值,否则寻找下个节点,最终在插入链表的时候会判断当前链表长度是否达到了转换成红黑树的条件(默认链表长度达到8时会转)。

  第四步,数据put成功后判断当前存储的数据大小是否超过了 threshold * loadFactor 的值,超过了就会执行resize方法:

/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

进行扩容的时候将所有的node节点进行hash计算e.hash & (newCap - 1),这样的结果不是在原来的就是在 当前位置加原来threshold长度的位置。至此整个put操作结束。

  get(Object key)的原理:

  弄懂了put操作之后,其实get就很容易理解了,首先根据传入key找到index,然后再对应的位置上获取就行了。

  最后,我看了HashMap源码之后,自己也手写了一个HashMap,不同之处在于我没有用到红黑树,而是使用二叉树代替,经测试插入1千万条uuid所需时间都差粗多,都是在二十几秒左右。关于二叉树与红黑树的区别可以自行百度,红黑树最主要解决的问题是在极端的情况下二叉树只有一条路径,时间复杂度位O(N),红黑树为了避免这种情况,每次都会自动调节树的深度,将最坏的情况的时间复杂度降低到O(logN)。

  因为完全是手写的,所以可能代码的可读性不是很好,但是基本的功能都能够实现了。如果大家有兴趣的话,可以下载过来看一下,也欢迎大家指出错误或提意见。项目地址: https://github.com/rainple1860/MyCollection

转载https://www.noblogs.cn/rainple/blog/17853.html

jdk1.8源码分析-hashMap的更多相关文章

  1. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  2. 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  3. 集合之LinkedHashSet(含JDK1.8源码分析)

    一.前言 上篇已经分析了Set接口下HashSet,我们发现其操作都是基于hashMap的,接下来看LinkedHashSet,其底层实现都是基于linkedHashMap的. 二.linkedHas ...

  4. 集合之HashSet(含JDK1.8源码分析)

    一.前言 我们已经分析了List接口下的ArrayList和LinkedList,以及Map接口下的HashMap.LinkedHashMap.TreeMap,接下来看的是Set接口下HashSet和 ...

  5. JDK1.8源码学习-HashMap

    JDK1.8源码学习-HashMap 目录 一.HashMap简介 HashMap 主要用来存放键值对,它是基于哈希表的Map接口实现的,是常用的Java集合之一. 我们都知道在JDK1.8 之前 的 ...

  6. 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)

    一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...

  7. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  8. 集合之TreeSet(含JDK1.8源码分析)

    一.前言 前面分析了Set接口下的hashSet和linkedHashSet,下面接着来看treeSet,treeSet的底层实现是基于treeMap的. 四个关注点在treeSet上的答案 二.tr ...

  9. 【1】【JUC】JDK1.8源码分析之ArrayBlockingQueue,LinkedBlockingQueue

    概要: ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞 注意这两个Condition即ReentrantLock的Co ...

随机推荐

  1. Day 01 计算机编程基础

    1.编程语言是什么? 编程语言是人与计算机交流的介质 2.什么是编程? 用编程语言写出一个个文件,这堆文件会达到一个目的 3.编程有什么用? 让计算机帮助我们干活,从而解放人类劳动力 4.计算机组成原 ...

  2. Codeforces Round #470 (rated, Div. 2, based on VK Cup 2018 Round 1)A. Protect Sheep

    http://codeforces.com/contest/948/problem/A   A. Protect Sheep Bob is a farmer. He has a large pastu ...

  3. vue2 在methods 中无法获取this对象

    在methods中使用箭头函数无法获取this ExamName:()=> { console.log(this);} 这样就行了: ExamName:function() { console. ...

  4. NotFoundHttpException

    报错:NotFoundHttpException 这种一般都是路由配置错误

  5. bzoj 1814 Fornula 1

    Formula 1 题意 在\(n*m\)的矩阵中,有些格子有树,没有树的格子不能到达,找一条回路,吃完所有的树,求有多少种方法. 解法 因为只要一条回路,所以我们必须维护插头的连通性. 具体的可以参 ...

  6. Shiro:整合swagger2时需要放行的资源

    filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resource ...

  7. js实现新闻滚动实例

    <html> <head> <meta http-equiv="Content-Type" content="text/html; char ...

  8. WinServer-IIS-URL重写

    WEB平台安装程序在Windows Server里面才有,在WIN7里面是没有的 然后在安装一个URL重写工具 然后再设置各种规则 来自为知笔记(Wiz)

  9. android canvas 画图笔记

    android canvas 画图笔记 1.PathEffect类 画虚线 Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setStyle(Paint.S ...

  10. HDU 3625

    有点置换群的味道. 当撞开一个门后,能打开一连串的门,即是可以排成一个圈.求的是种数,于是,可以使用第一类斯特林数,求出撞了0~K次的种数. 但是,注意,当第一个门为独自一个圈时,是不可行的,因为这代 ...