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. word转html预览

    #region Index页面 /// <summary> /// Index页面 /// </summary> /// <paramname="url&quo ...

  2. web前端-bootstrap

    1.什么是bootstrap 前端开发开源工具包 ,包含css样式库+jq插件 ,ui效果非常好 ,都是通过给标签加class选择器来实现功能的 2.特点 响应式布局:使用栅格系统可以把页面呈现在不同 ...

  3. 安卓微信对接H5微信支付出现“商家参数有误,请联系商家解决”的问题处理

    最近遇到客户在对接我们微信支付的时候,一些商家反馈在用户支付的过程中会出现报错,出错的截图如下: 查看微信官方文档如下:https://pay.weixin.qq.com/wiki/doc/api/H ...

  4. git stash与git commit的区别

    问题的出现    写这篇文章的缘由是在工作中初次使用Git的时候遇到了一个奇怪的现象,即每次提交代码的时候,如果没有及时拉取代码就会导致本地库的代码不是最新的,这样自己修改代码之后想要push到远程仓 ...

  5. 【原】通过Spring结合Cglib处理非接口代理

    前言: 之前做的一个项目,虽然是查询ES,但内部有大量的逻辑计算,非常耗时,而且经常收到JVM峰值告警邮件.分析了一下基础数据每天凌晨更新一次,但查询和计算其实在第一次之后就可以写入缓存,这样后面直接 ...

  6. Mac启动MySQL

    启动MySQL服务 sudo /usr/local/Cellar/mysql//bin/mysql.server start 停止MySQL服务 sudo /usr/local/Cellar/mysq ...

  7. HBase常用的JAVA API操作

    为了方便以后查看,总结了一些常用的java操作hbase的代码: package com.mcq; import static org.hamcrest.CoreMatchers.describedA ...

  8. dstat 系统监控命令

    tat 命令很强大,可以实时监控CPU,磁盘,网络,IO,内存等. yum install -y dstat 例: dstat  #查看全部监控信息 dstat -c  #查看cpu 使用情况

  9. 上云测试,这些关键点你get 到没有

    导读,先从云化说起,再谈谈云化形态下,除了常规的功能测试,云化的测试,还需要有几个必须要get到的硬核指标,最后在分别详解这些关键点硬核指标是什么,和如何测试呢.这是个值得深思的问题,希望所有测试人都 ...

  10. mysql基础sql进阶

    回顾前面的基础命令语句 修改数据表 添加字段: alter table 表名 add 字段名 列类型[not null|null][primary key][unique][auto_incremen ...