HashMap作为常用的一种数据结构,阅读源码去了解其底层的实现是十分有必要的。在这里也分享自己阅读源码遇到的困难以及自己的思考。

HashMap的源码介绍已经有许许多多的博客,这里只记录了一些我看源码过程中的疑问,一些基础知识不再讲解。

一:Hash值的来源和使用 

 public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} static final int hash(Object key) {
int h;
// <<< 无符号右移
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

    这里是put()方法,里面有一个调用hash(key)就是得到hash值:

  如果key为null,则返回0。否则则返回 key的(h = key.hashCode()) ^ (h >>> 16)(hashCode 异或 hashCode无符号右移16位),既二次散列,这么做的原因是为了尽可能的分散到桶(数组)各个位置,避免数据扎堆放在一个桶里面。提高HashMap运算效率。其中HashCode()是本地方法,不同的jvm会有不同的结果。

  例 hashcode :  0001 1000 0001 0001 1111 0001 0110 0000  ^ 0000 0000 0000 0000 0001 1000 0001 0001 

  相当于hashCode()的高16位异或低16位。这样就相当于32位数据都参与到了Hash运算。这样使得hash更加散列,尽可能的桶寻址更分散。

  

  这里有专门的传送门http://blog.csdn.net/anxpp/article/details/51234835。

  其实按照我的理解,无符号

  得到了key的hash值,又是如何运用的哪?下面的代码不需要看那么多,如果有闲心可以看一看~

   在putVal()方法中第9行代码,if ((p = tab[i = (n - 1) & hash]) == null),通过(n-1 & hash)与运算得到下标位置,这就是根据hash值得到了桶(数组)的位置。 

        &操作同时也保证了不会数组越界,(n-1)是桶(数组)界限。

  Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table 是否为空,初始化或者加倍表的大小。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//i = (n - 1) & hash ,计算出来下标,这个下标为空,说明没有被占用,直接newNode.
8 //没有发生Hash碰撞
9 if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,相同 e = p;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {// 该链为链表
//遍历table[i],判断链表长度是否大于TREEIFY_THRESHOLD(默认值为8),
//大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
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))))
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;
}

  如果对hashCode感兴趣的话,可以开这个门https://www.cnblogs.com/dolphin0520/p/3681042.html

二 HashMap的扩容resize()

  看代码就是一个循序渐进的过程。

  我们知道桶(数组)的下标是根据(n-1)&hash得到的,当HashMap扩容后(n-1)就会发生变化啊,这样不就会扩容后寻不到下标了吗?

没错,这是一个很简单的问题,但是我看的时候没有看全就回家了,在地铁上想了一路怎么解决,然后猜测是扩容后会重新把所有数据在计算一遍。回到家后,我就去看代码对这个想法进行验证。

  确实如此,所以我觉得扩容真心费劲。这里贴一下resize()方法。比较长~

  从第30行就开始处理数据,使其根据新的容量(n-1)重新分配下标。当然,分配也不是漫无目的的:

  在第56,57行代码中中:

  Node<K,V> loHead = null, loTail = null;//没有改变索引位置的记录loHead【链表】,loTail 当前链表的尾节点
Node<K,V> hiHead = null, hiTail = null;//改变索引位置的记录hiHead【链表】,hiTail 当前链表的尾节点
 一个原来数据重新分配后,只有两个位置可以去~
  1. 原封不动的还在原来下标 newTab[j]
  2. 换新的下标,但是位置是固定的 newTab[j + oldCap]
这是什么原因导致的那?
  我们知道,HashMap的桶(数组)扩容是扩容为原来的两倍
(newCap = oldCap << 1)。也就是说,原来的(n-1)是1111,现在成了1 1111 ,而key的hash是不会变的。
  两者再次进行&运算,要么,多一个高位1。要么不变。例如:

  每次扩容都会将全部元素计算一遍,所以扩容的开销还是很大的。

  一篇传送门,如果不是很懂,可以看这里~http://blog.csdn.net/bnmb888/article/details/77164485


  1 final Node<K,V>[] resize() {
         Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//得到原数组(哈希桶)长度
int oldThr = threshold;//原来所能容纳的key-value对极限,阈值。
int newCap, newThr = 0;//新的长度,新的阈值。
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//超过了hashMap最大(哈希桶)容量
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 哈希桶长度为0,且初始化HashMap时设置了长度
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(保持次序)
//原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”,即newTab[j + oldCap]
/**
* 示例1:
e.hash=10 0000 1010
oldCap=16-1 0000 1111
                   newCap=32-1 0001 1111
&old=0 0000 1010 比较高位的第一位
                    &new=0 0000 1010
结论:元素位置在扩容后数组中的位置没有发生改变 放入 loHead
          
示例2:
e.hash=17 0001 0001
oldCap=16-1 0000 1111
                   newCap=32-1 0001 1111
&old =0 0000 0001 比较高位的第一位
                   &new =1 0001 0001
结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是[原下标位置+原数组长度] 放入 hiHead
*
*/
Node<K,V> loHead = null, loTail = null;//没有改变索引位置的记录loHead【链表】,loTail 当前链表的尾节点
Node<K,V> hiHead = null, hiTail = null;//改变索引位置的记录hiHead【链表】,hiTail 当前链表的尾节点
Node<K,V> next;
do {
next = e.next;
// 索引还是原索引
if ((e.hash & oldCap) == 0) {//如:oldCap 是 16,即二进制 1 0000 ,(1111),相与,可以判断e.hash的高位是否是0。为0则进入if语句
if (loTail == null)//loHead链表首位为null
loHead = e;//链表首位放入e
else
loTail.next = e;//依次放入节点,保持次序。
loTail = e;//记录当前节点位置
}
//索引改为 【原索引+oldCap】
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//原索引位置放入整个loHead链表
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//原索引+oldCap位置放入整个hiHead链表
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

 三 HashMap的红黑树操作

  JDK1.8以后,当HashMap的链表过长时(TREEIFY_THRESHOLD = 8;),会将链表转化为红黑树。在putVal()方法中有介绍。

  当链表(UNTREEIFY_THRESHOLD = 6)会将其拆分,但是仅仅是在resize()的时候会有这一步操作,remove并不会。

   38行: ((TreeNode<K,V>)e).split(this, newTab, j, oldCap)
该方法描述为:调整树结构,树太小拆分掉。 仅从调整大小的时候调用;
 

这个方法是寻找树节点  


     final TreeNode<K,V> getTreeNode(int h, Object k) {
            //当前节点的父节点是否为null, 不为null,寻找根结点(root()),为null,当前即为根结点 
            return ((parent != null) ? root() : this).find(h, k, null);
        }
  final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//p是调用这个方法的对象
//getTreeNode(int h, Object k), p是root对象
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
//hash值,对于任何给定的object,hash值是相同的
if ((ph = p.hash) > h) //当前节点hash值大于寻找的哈希值,寻找左孩子
p = pl;
else if (ph < h)//当前节点hash值小于寻找的哈希值,寻找右孩子
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))//hash值相同,key值相同,返回该节点p
return p;
else if (pl == null)//key值不同,左孩子为null(红黑树的叶子节点),p改为右孩子。红黑树中,为null说明已经到达叶子节点,所以转向pr
p = pr;
else if (pr == null)//key值不同,右孩子为null(红黑树的叶子节点),p改为左孩子。
p = pl;
else if ((kc != null || //自定义的比较(实现comparable接口的类
(kc = comparableClassFor(k)) != null) && //Object k(map的key)是否实现了comparable接口,是的话返回该实现类
(dir = compareComparables(kc, k, pk)) != 0) //在kc类中 调用k.compareTo(pk),自定义方法,根据返回值决定去左孩子还是右孩子
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)//hash值相同,key值不同且左孩子右孩子都存在,递归find右孩子
return q;
else //hash值相同,key值不同且左孩子右孩子都存在,右孩子没有找到,find左孩子
p = pl;
} while (p != null);//是否到叶子节点,红黑树叶子节点均为null
return null;
}

更多的HashMap知识,以后有空会继续更新。Java源码所写的注释,上传到了GitHub。会持续更新注释内容。

https://github.com/coldwindYBMC/Java_source

 https://i.cnblogs.com/EditPosts.aspx?postid=8214358

  

Java源码解读(一)——HashMap的更多相关文章

  1. Java源码系列2——HashMap

    HashMap 的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析.能力有限,欢迎指正. HASH 值的计算 前置知识--位运算 按位异或操作符^:1^1=0, 0^0=0, 1^0=0, 值 ...

  2. 【java集合框架源码剖析系列】java源码剖析之HashMap

    前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...

  3. Java 源码刨析 - HashMap 底层实现原理是什么?JDK8 做了哪些优化?

    [基本结构] 在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的: JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,它的 ...

  4. [java源码解析]对HashMap源码的分析(二)

    上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得“拉链法 ...

  5. [java源码解析]对HashMap源码的分析(一)

    最近有空的时候研究了下HashMap的源码,平时我用HashMap主要拿来当业务数据整理后的容器,一直觉得它比较灵活和好用, 这样 的便利性跟它的组成结构有很大的关系. 直接开门见山,先简要说明一下H ...

  6. Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?

    我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随 ...

  7. Java源码解读(一) 8种基本类型对应的封装类型

    说起源码其实第一个要看的应该是我们的父类Object,这里就不对它进行描述了大家各自对其进行阅读即可. 一.八种基本类型 接下来介绍我们的八种基本类型(这个大家都知道吧):char.byte.shor ...

  8. 【java集合框架源码剖析系列】java源码剖析之HashSet

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...

  9. Java源码系列1——ArrayList

    本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析.能力有限,欢迎指正. ArrayList是什么? ArrayList 就是数组列表,主要用来装载数据.底层实现是数组 Obj ...

随机推荐

  1. JavaSE之绘制菱形

    在JavaSE的算法练习中,绘制菱形是一个比较常见的案例.菱形效果如下图所示: 我们在解决算法问题时,通常情况下,先不要急于马上编码,而是要先观察,找出解决问题的关键所在. 在上图中,我们可以看到,菱 ...

  2. 使用SSH快速下载Git项目

    文章首发于[博客园-陈树义],点击跳转到原文使用SSH快速下载Git项目. Git下载项目的几种方式 Git是常用的代码版本技术,而GitLab则是开源的Git版本管理软件,GitLab是最受欢迎的版 ...

  3. Scala入门系列(五):面向对象之类

    定义类 // 定义类,包含field以及method class HelloWorld { private var name = "Leo" def sayHello() { pr ...

  4. git以及github的初级入门(一)

    本身学习git的操作是没什么兴趣的,毕竟原本是win平台学的java开发,git下那么多复制的命令行操作确实比较让人头疼,直到昨天我打开计算机的时候,我放置项目的E盘,以及F盘,G盘盘符都不见了!!我 ...

  5. javaweb学习总结(四)——Http协议(转)

    转载自 http://www.cnblogs.com/xdp-gacl/p/3751277.html 一.什么是HTTP协议 HTTP是hypertext transfer protocol(超文本传 ...

  6. Android设计模式(九)--外观模式

    问题:在Android中,Apk能够有微信,QQ为代表的插件式安装更新功能: 那么问题来了,主系统(姑且这么说)调用插件式安装的子系统.由子系统提供对外的訪问.属不属于一种外观模式呢? 先说设计模式: ...

  7. java注解细节

    现在很多框架都使用注解了,典型的像Spring里面就可以看到大量的注解,比如@Service,@Controller这一类的都是注解.注解Annotation是java一项很重要的功能.下面就来整理一 ...

  8. 用maven建立java web项目

    1.在eclipse的菜单栏选择File->New->Other->Maven->Maven ,并在第一个框打勾,然后点击下一步 2.转换为java的Dynamic Web P ...

  9. json api

    from flask import Flask, redirect, url_for, jsonify, request app = Flask(__name__) users = [] ''' RE ...

  10. android 使用Ksoap2工具类实现WebService网络编程

    1.下载Ksoap2,将jar包拷贝到libs目录下.然后右键点击拷贝进来的jar,在弹出菜单中点击Add As Library. 2.在AndroidManifest.xml中添加访问网络的权限 & ...