HashMap 源码分析  基于jdk1.8分析

1:数据结构:

transient Node<K,V>[] table;  //这里维护了一个 Node的数组结构;

下面看看Node的数据结构,Node是它的一个内部类:

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;  //hash值

final K key;    //key

V value;      //value

Node<K,V> next;   //后继节点的引用

Node(int hash, K key, V value, Node<K,V> next) {

this.hash = hash;

this.key = key;

this.value = value;

this.next = next;

}

Node是一个维护了后继节点的单链表结构;

从这里可以看出HashMap是一个 数组+单链表 的数据结构;

下面用图表示数据结构:

2:接下里看看成员变量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //  默认数组的长度,16;

static final int MAXIMUM_CAPACITY = 1 << 30;      //  最大容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;    //  加载因子

static final int TREEIFY_THRESHOLD = 8;           //  链表转化为红黑树的链表长度

static final int UNTREEIFY_THRESHOLD = 6;         //  红黑树转为链表的系数

static final int MIN_TREEIFY_CAPACITY = 64;

3:接下里看看构造方法:

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR; // 加载因子赋值为0.75 其他成员变量为默认

}

构造方法中看出初始话了一个长度为 0 的Node数组

4:几个重要的方法分析  get  put   delete,这里不对红黑树的具体做分析

put分析: 这里为第一次put  key=name  value=aa

public V put(K key, V value) {   //key=name   value=aa

return putVal(hash(key), key, value, false, true);

}

hash(key)  //取key 的hash值: 3373752

下面分析putVal的方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) { //hash= 3373752 value=aa  onlyIfAbsent=fase

Node<K,V>[] tab; Node<K,V> p; int n, i;

// table 第一次put table=null

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;   // 进入resize() 进行扩容

//这里简单说明扩容方法:原始Node数组为空则创建一个长度为16的链表Node数组,如果Hashmap中有数据,将原始链表数组中的数据重新散列到新的数组链表中,具体操作会在下面的扩容中说明

if ((p = tab[i = (n - 1) & hash]) == null)  //n=16   hash= 104583484 相当于取模操作

//若Node[] 数组中没有元素 创建新的node,Node中包含 key的hash,key,value值,添加到tab[i]中

tab[i] = newNode(hash, key, value, null);

else {  //下面是Node数组中有值的情况

Node<K,V> e; K k;

if (p.hash == hash &&  //这是添加重复元素的情况,进行覆盖

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

else if (p instanceof TreeNode)  //这里链表中已经是红黑树的结构的处理

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);// 使用拉链法解决hash碰撞

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;

}

get 分析:get方法比较简单,就是获取数组中的Node对象后进行遍历

下面具体看看源码:

public V get(Object key) {

Node<K,V> e;

// get方法主要是执行下面的getNode的方法

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

//下面进入getNode 方法进行分析:

final Node<K,V> getNode(int hash, Object key) {  // hash=104583485 key=name1

Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

if ((tab = table) != null && (n = tab.length) > 0 &&  // 判空操作

(first = tab[(n - 1) & hash]) != null) {

//最先判断链表中的第一个元素是否是要查找的元素

if (first.hash == hash && // always check first node

((k = first.key) == key || (key != null && key.equals(k))))

return first;  // 如果只要一个first元素则返回first Node

if ((e = first.next) != null) { //若链表中有多个Node元素

if (first instanceof TreeNode)  //若为数结构则进行树结构查找

return ((TreeNode<K,V>)first).getTreeNode(hash, key);

do {

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

return e;  //遍历链表中的Node 元素,找到相应key的node

} while ((e = e.next) != null);

}

}

return null;

}

到这里hashmap 的get 方法分析完了;

remove分析:这里remove方法分析也不具体设计红黑树数据结构的平衡操作;

public V remove(Object key) {  //key=name1

Node<K,V> e;

return (e = removeNode(hash(key), key, null, false, true)) == null ?

null : e.value;

}

接下来看看 removeNode(hash(key), key, null, false, true) 这个方法

final Node<K,V> removeNode(int hash, Object key, Object value,  // hash 104583485  key name1

boolean matchValue, boolean movable) {

Node<K,V>[] tab; Node<K,V> p; int n, index;

if ((tab = table) != null && (n = tab.length) > 0 &&

(p = tab[index = (n - 1) & hash]) != null) { // 链表第一个元素赋值给p

Node<K,V> node = null, e; K k; V v;

if (p.hash == hash &&  //查找第一个元素即为要删除的元素

((k = p.key) == key || (key != null && key.equals(k))))

node = p;  //p 赋值给node

else if ((e = p.next) != null) {  //若第一个元素不是需要需要或删除的元素则比那里链表查找找到需要删除的元素的位置

if (p instanceof TreeNode)

node = ((TreeNode<K,V>)p).getTreeNode(hash, key);

else {

do {

if (e.hash == hash &&

((k = e.key) == key ||

(key != null && key.equals(k)))) {

node = e;

break;

}

p = e;

} while ((e = e.next) != null);

}

}

if (node != null && (!matchValue || (v = node.value) == value ||

(value != null && value.equals(v)))) {

if (node instanceof TreeNode)  //红黑树删除

((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

else if (node == p) //链表删除

tab[index] = node.next;

else

p.next = node.next;

++modCount;

--size;

afterNodeRemoval(node);

return node;

}

}

return null;

}

至此,hashmap 的删除操作已经分析结束了,主要的步骤是先查找再对next 引用指向next.next 完成删除的操作;

5:hash碰撞的解决

对于hashmap的put操作来说,其实就是将元素(Node)添加到 Node[]数组中,这里需要添加的Node应该放置在Node[] 哪个位置呢?

1):计算Node应该添加到Node[] 数组中的索引,通过下面处理获得:

if ((p = tab[i = (n - 1) & hash]) == null) 通过hash和16取模获得索引位置 i

假设i=3,假设 Node[3] 中已经存在Node,遍历node链表,并将元素插入到链表的最后一个位置的next引用中

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);  //添加node 元素到链表最后一个元素的next引用中

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;

}

总结:hashmap 解决hash冲突的方式的进行拉链发,将元素进行node 节点的next指向组成单链表的数据结构,当链表的长度超过8的时候,则转化为红黑树的方式来进行存储

6:扩容的实现

Hashmap 的扩容是通过resize的方法实现的:

下面分析resize 的方法几个步骤:

1): Node<K,V>[] oldTab = table;  //原始的Node[] 数组进行暂存,后续需要操作

判断旧的Node[] 长度超过最大的容量则返回原始原数组

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

接下来将 oldCap 和 oldThr 的数值翻倍 并赋值给新的变量

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

newThr = oldThr << 1; // double threshold

创建一个大小为新容量的Node数组

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

记下来就是遍历暂存起来的旧的Node数组,将旧数组中的值添加到新的Node[] 中:

代码处理看着有点长,其实很简单的逻辑

for (int j = 0; j < oldCap; ++j) {  //遍历旧的 Node[] 数组

Node<K,V> e;

if ((e = oldTab[j]) != null) { //索引为j 中的链表是否为空

oldTab[j] = null;

if (e.next == null)  //判断链表是否只有一个Node

newTab[e.hash & (newCap - 1)] = e;  //元素的hash取模于新的Node[] 数组长度,找到e元素应该放置在Node[] 数组中的位置

else if (e instanceof TreeNode) //若为红黑树则进行树结构处理

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else { // 下面这段逻辑主要就是当数组索引为j的位置中Node节点有后继节点的情况下,遍历node链表,将节点放置到新的数组中相应的位置中

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;

}

}

}

}

总结:这里数组扩容主要的操作就是将Node数组扩容成原先的两倍,扩成2倍的目的是为了减少hash碰撞。

这里hashmap 的基本的原理已经分析结束了。

HashMap 源码分析 基于jdk1.8分析的更多相关文章

  1. HashMap源码和并发异常问题分析

    要点源码分析 HashMap允许键值对为null:HashTable则不允许,会报空指针异常: HashMap<String, String> map= new HashMap<&g ...

  2. 【源码】HashMap源码及线程非安全分析

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

  3. HashMap源码剖析及实现原理分析(学习笔记)

    一.需求 最近开发中,总是需要使用HashMap,而为了更好的开发以及理解HashMap:因此特定重新去看HashMap的源码并写下学习笔记,以便以后查阅. 二.HashMap的学习理解 1.我们首先 ...

  4. hashmap源码解析,JDK1.8和1.7的区别

    背景:hashmap面试基础必考内容,需要深入了解,并学习其中的相关原理.此处还要明白1.7和1.8不通版本的优化点. Java 8系列之重新认识HashMap Java 8系列之重新认识HashMa ...

  5. 【Java并发集合】ConcurrentHashMap源码解析基于JDK1.8

    concurrentHashMap(基于jdk1.8) 类注释 所有的操作都是线程安全的,我们在使用时无需进行加锁. 多个线程同时进行put.remove等操作时并不会阻塞,可以同时进行,而HashT ...

  6. HashMap源码解读(jdk1.8)

    1.相关常量 默认初始化容量(大小) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 最大容量 static final int M ...

  7. HashMap源码解析(JDK1.8)

    package java.util; import sun.misc.SharedSecrets; import java.io.IOException; import java.io.Invalid ...

  8. HashMap源码之常用方法--JDK1.8

    常用方法 hash(key) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCo ...

  9. HashMap源码之构造函数--JDK1.8

    构造函数 变量解释 capacity,表示的是hashmap中桶的数量,初始化容量initCapacity为16,第一次扩容会扩到64,之后每次扩容都是之前容量的2倍,所以容量每次都是2的次幂 loa ...

随机推荐

  1. ASP.NET 中关GridView里加入CheckBox 在后台获取不到选中状态的问题

    <!-- 在GridView里添加CheckBox选择控件 !--> <ItemTemplate> <asp:CheckBox ID="CheckBox&quo ...

  2. Java网络编程 -- Netty中的ByteBuf

    由于JDK中提供的ByteBuffer无法动态扩容,并且API使用复杂等原因,Netty中提供了ByteBuf.Bytebuf的API操作更加便捷,可以动态扩容,提供了多种ByteBuf的实现,以及高 ...

  3. Android自定义圆角矩形进度条2

    效果图: 或 方法讲解: (1)invalidate()方法 invalidate()是用来刷新View的,必须是在UI线程中进行工作.比如在修改某个view的显示时, 调用invalidate()才 ...

  4. apk分析 1

    配置抓包工具 关闭捕获主机通讯关闭 配置: 在手机端进行配置 进入wifi设置,长按网络高级选项->手动设置代理 测试是否设置成功,手机上随便开应用看抓包器是否有反应 打开抓包目标apk(恋恋, ...

  5. 【已解决】git的一些常用命令

    git:分布式的版本管理系统,一般的开发模式: 如果是开发人员,忽略此步骤,从下面大字的开始即可: 项目开始阶段,初始化项目(init),提交本地的代码到仓库,将本地仓库的代码推送到远端库(push) ...

  6. 统计代码测试覆盖率-Python

    衡量Unit Test(单元测试)是否充分, 覆盖率是一个必要指标, 是检验单元测试的重要依据, 这里针对python unittest 的单元测试覆盖率coverage进行分享. 来自官方的解释: ...

  7. FollowUp CRM是什么,有什么作用,好不好

    FollowUp,基于Gmail的私人CRM: 是一款Chrome插件,构建在Gmail邮箱服务之上: FollowUp支持通过Gmail:设置提醒,编写备注,计划会议,查看下一步的内容等: Foll ...

  8. 201871010108-高文利《面向对象程序设计(java)》第七周学习总结

    项目 内容 这个作业属于哪个课程 <任课教师博客主页链接> https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 <作业链接地址> ht ...

  9. 解决Android中AsyncTask的多线程阻塞问题

    Android开发中执行耗时操作并更新UI时,通常有三种方式:1.直接调用runOnUiThread(new Runnable(){}),使用简单,但不能在Activity之外的环境使用,如View. ...

  10. 关于setImageURI out of memory的一些解决办法

    http://stackoverflow.com/questions/477572/strange-out-of-memory-issue-while-loading-an-image-to-a-bi ...