转自:http://blog.csdn.net/zdsicecoco/article/details/51775545

一 什么是HashMap,HashMap的工作原理

HashMap是一个基于哈希表的Map接口实现。

首先先看一下,在JDK1.8的时候,HashMap数据存储jniego

工作原理

当我们使用put(KEY,VALUE)存储键值对象到HashMap   ,首先会对

Key进行哈希计算,就是调用hashcode方法,如果key为空,hash值是0. 并且对hash结果进行位异或运算。

staticfinal int hash(Objectkey) {

int h;

return (key ==null) ? 0 : (h =key.hashCode()) ^ (h >>> 16);

}

比如h的值是15 那么就是 key=null?0:(15^(h>>>16));h较小的时候一般为0。

HashMap有一个Node[]数组,即哈希桶数组。Node是啥呢?

staticclass Node<K,V> implements Map.Entry<K,V> {

final inthash;

final K key;

V value;

Node<K,V> next;

Node(inthash, K key, Vvalue, Node<K,V>next) {

this.hash =hash;

this.key =key;

this.value =value;

this.next =next;

}

其实Node就是一个基于单向链表数据结构的存储key和value的一个对象。next指向下一个Node.实现了Map.Entry接口

HashMap有几个比较重要的属性:

intthreshod; //存储键值对的极限
final float loadFactor;//加载因子

intmodCount;

intsize;

首先Node[]哈希桶数组默认长度是16,加载因子是0.75。

加载因子是对空间和时间效率的一个平衡,不建议修改。如果内存足够,并且对时间效率要求较高,可以设置大点。可以大于1.

threshold是所能容纳的Node最大数量。threshod=length*loadFactor.比如这里就是16*0.75=12.一旦超过这个threshod这个阀值,就要重新计算size.也就是调用resize方法。

size:当前map时机存储的键值对的数量。他和哈希桶的长度应该区别开。

modCount:主要用来记录HashMap   内部结构发生变化的次数

哈希桶数组的长度length大小必须是2的n次方,一般常规做法是把桶设计为素数,因为素数导致冲突的概率小很多。

即使hash算法设计的再合理,也避免不了链过长的情况,一旦出现链过长,那就查询很慢,严重影响HashMap性能。于是1.8对数据结构作了进一步的优化,引入红黑树,默认超过8时,链表就转化为红黑树,利用红黑树快速增删改查的特点提高hashMap性能。

Put方法分析:

  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向第6步;如果table[i]不为空,转向第3步
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向第四步,这里的相同指的是hashCode以及equals
  4. 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向第5步;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

final V putVal(inthash, K key, Vvalue, booleanonlyIfAbsent, booleanevict) {

Node<K,V>[]tab;

Node<K,V>p;

int n,i;

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

n = (tab = resize()).length;//tab为null或者长度为0,则resize

//i = (n - 1) & hash 计算存储哈希桶数组的下标

//根据这个下标判断当前这个位置有无Node元素,如果没有则创建Node

if ((p =tab[i = (n - 1) &hash]) == null)

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

else {

Node<K,V>e; K k;

//判断key是否存在且key是否和链表第一个Node的key相同,如果相同,则覆盖

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

e = p;

//hash不同,或者key不一样,则判断该链是否是红黑树,如果是添加到红黑树

else if (pinstanceof TreeNode)

e = ((TreeNode<K,V>)p).putTreeVal(this,tab, hash, key, value);

//否则该链为链表

else {

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

//判断next是否为空

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

//如果为空创建Node,并赋给next

p.next = newNode(hash,key, value,null);

//如果链表长度大于8,转为红黑树处理

if (binCount >=TREEIFY_THRESHOLD - 1)// -1 for 1st

treeifyBin(tab,hash);

break;

}

//判断下一个node的hash和key,如果key相同且hash相同,则覆盖

if (e.hash ==hash &&

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

break;

p = e;

}

}

if (e !=null) { // existing mapping for key

VoldValue = e.value;

if (!onlyIfAbsent ||oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

if (++size >threshold)

resize();

afterNodeInsertion(evict);

return null;

}

二 HashMap扩容机制:

向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。由于Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

JDK1.7实现

void resize(int newCapacity) {   //传入新的容量

Entry[] oldTable = table;    //引用扩容前的Entry数组

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了

threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了

return;

}

Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组

transfer(newTable);                       //将数据转移到新的Entry数组里

table = newTable;                         //HashMap的table属性引用新的Entry数组

threshold = (int)(newCapacity * loadFactor);//修改阈值

}

void transfer(Entry[] newTable) {

Entry[] src = table;                   //src引用了旧的Entry数组

int newCapacity = newTable.length;

for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组

Entry<K,V> e = src[j];             //取得旧Entry数组的每个元素

if (e != null) {

src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)

do {

Entry<K,V> next = e.next;

int i = indexFor(e.hash, newCapacity); //重新计算每个元素在数组中的位置

e.next = newTable[i]; //标记[1]

newTable[i] = e;      //将元素放在数组上

e = next;             //访问下一个Entry链上的元素

} while (e != null);

}

}

}

三 HashMap线程不安全,为什么?

我们知道,HashMap在插入数据的时候,会先检查size有没有超过threshold这个阀值的大小,如果超过了,需要进行resize 扩容操作。

并且把老的哈希桶数组迁移到扩容之后的哈希桶数组中。

我们举个例子:

publicclass Calcu {

privatestatic HashMap<Integer, String> map =new HashMap<Integer, String>(2, 0.75f);

publicstatic void main(String[]args) {

map.put(5, "C");

new Thread("Thread1") {

public void run() {

try {

Thread.sleep(5);

}catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

map.put(7, "B");

System.out.println(map);

};

}.start();

new Thread("Thread2") {

public void run() {

map.put(3, "A");

System.out.println(map);

};

}.start();

}

}

我们先让主线程走,线程一二在run方法第一行挂起,然后在放开线程一二结果是:

void resize(intnewCapacity) {

Entry[] oldTable = table;

int oldCapacity =oldTable.length;

if (oldCapacity ==MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];

transfer(newTable);

table = newTable;

threshold = (int)(newCapacity *loadFactor);

}

void transfer(Entry[]newTable) {

Entry[]src = table;

int newCapacity =newTable.length;

for (intj = 0; j < src.length; j++) {

Entry<K,V>e = src[j];

if (e !=null) {

src[j] = null;

do {

Entry<K,V>next = e.next;

int i = indexFor(e.hash,newCapacity);

e.next = newTable[i];

newTable[i] =e;

e = next;

}while (e !=null);

}

}

}

然后断点处设在transfer方法第一行,然后线程一二都在此处挂起。

然后再把断点设在Entry<K,V> next = e.next; 此时线程一挂起,线程二断点设在run方法最后一行代码。

此时线程二已经完成rehash操作,链表重组完成,结果如图:

但是这个时候,线程一的e指向的还是key(3) 然后key(3)的next是key(7),指向了新的链表。

放开线程一后:

Entry<K,V>next = e.next;

=>next=key(3).next=key(7);

=>next = key(7);

inti = indexFor(e.hash, newCapacity);

结果i=3;

e.next = newTable[i];

=> e.next = null,也就是说key(3).next =null;

newTable[i] = e;

=>把key(3) 赋给newTable[3]

e = next;

此时e=key(7),即e指向key(7);

如此,下一次循环,处理key(7),可知

Entry<K,V>next = e.next; //此时key(7)的next就是key(3)

=>next=key(7).next=key(3);

inti = indexFor(e.hash, newCapacity);

结果i=3;

e.next = newTable[i];

=> e.next = key(3),也就是说key(7).next = key(3);

newTable[i] = e;

=>把key(7) 放入桶newTable

e = next;

此时e=key(3),即e指向key(3);

这个过程之后,死循环出现。结果如图示:

四 两个对象的hashcode相同,为有什么结果?如果hashcode相同,如何获取值

如果hashcode相同,发生碰撞的可能性很大。一般会采用开放地址法或者链地址法来解决hash冲突。

先根据hashcode得到元素应该存在哪一个位置,然后找到位置之后,插入到链表中。

如果这样,如何获取值呢?

通过get(key)来获取,在获取的时候,会对key进行一个比较,判断是不是同一个,比较依据就是key的equal方法来判断。

JDK1.8 HashMap 深入理解相关推荐

  1. 红黑树 键值_大厂面试官:说一下JDK1.8 HashMap有哪些亮点?

    上篇我们介绍了JDK1.7版的HashMap,今天我们来讲解下JDK1.8版的HashMap. JDK1.7的实现大家看出有没有需要优化的地方? 其实一个很明显的地方就是:当 Hash 冲突严重时,在 ...

  2. HashMap底层理解(上)

    本来想先在专栏里简单的说一下二叉树,红黑树的内容后再说HashMap的,但看到评论区里不断的出现HashMap这个词,怕大家等得着急,本篇文章就先说说HashMap吧,前面讲ArrayList和Lin ...

  3. jdk1.8 HashMap ConcurrentHashMap

    JDK1.8逐字逐句带你理解ConcurrentHashMap https://blog.csdn.net/u012403290 JDK1.8理解HashMap https://blog.csdn.n ...

  4. Java--敲重点!JDK1.8 HashMap特性及底层数组+单链表+红黑树知识(建议收藏)

    ❤️‍大家好,我是贾斯汀!❤️‍ 学习目录 学习背景 HashMap特性 HashMap添加元素四步曲 前奏:HashMap如何添加一个元素? 第一步曲:根据key得到hashCode值 第二步曲:根 ...

  5. JDK1.8 HashMap源码分析

    HahsMap实现了Map接口.其继承关系如下图: HashMap有两个影响性能的重要参数:初始容量和加载因子.容量是Hash表中桶的个数,当HashMap初始化时,容量就是初始容量.加载因子是衡量h ...

  6. JDK源码分析--HashMap深入理解

    一.实现原理 以JDK1.7源码为例进行分析 (一)Hashing的概念 将字符串转换成固定长度(一般是更短的长度)的数值或索引值的方法,也称为散列法或哈希法.常用于数据库中建索引,或是用于各种加解密 ...

  7. 浅谈对HashMap的理解,以及对HashMap部分源码的分析

    文章目录 一.什么是HashMap 1.1 Hash是什么 1.2 Map是什么 Map的特点 Map和Hash的结合 二.HashMap部分源码理解 2.1 关键变量 2.2 关键逻辑 2.3 关键 ...

  8. JDK1.8 HashMap源码解析(不分析红黑树部分)

    一.HashMap数据结构 HashMap由 数组+链表+红黑树实现,桶中元素可能为链表,也可能为红黑树.为了提高综合(查询.添加.修改)效率,当桶中元素数量超过TREEIFY_THRESHOLD(默 ...

  9. HashMap原理理解

    HashMap是基于hashing原理来实现的,在java中是通过Map接口来实现的,以key-value形式存在.通过get和set方法来获取和存储对象.当我们把键值传递给put方法时,它会调用键对 ...

最新文章

  1. 指南:从学者到创业者
  2. oracle对大对象类型操作:blob,clob,nclob,bfile
  3. leetcode 1723. 完成所有工作的最短时间(二分+剪枝+回溯)
  4. ug建模文本怎么竖着_入门到成为UG编程高手,这些步骤你不得不了解
  5. php 交换,php变量交换
  6. 今日头条把微信按在地上摩擦
  7. pythonsocket自定义协议_Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服务器...
  8. 微信小程序跳转视频号直播
  9. 虚拟机无法启动服务器失败,Hyper-V虚拟机无法启动故障图解
  10. 66个求职应聘技巧性问答(三)
  11. 入门必学 | R语言参数检验之t检验与方差分析
  12. 工具及方法 - 编辑二进制文件(使用VSCode和Notepad++的插件Hex Editor)
  13. 大数据分析案例-对电信客户流失分析预警预测
  14. 我用python写了个小病毒,老板再也不敢扣我工资啦
  15. SQL-常用SQL语句
  16. Linux命令之dos2unix
  17. 简单实用的ajax脚本
  18. “测不准原理”在计算机领域的体现
  19. ubuntu能连接wifi或手机USB共享热点,不能上网
  20. jquery显示隐藏元素

热门文章

  1. 嵌入式开发有年龄限制吗_报名深圳成考有年龄限制吗?
  2. Python3+TensorFlow人脸识别:1-1课程导学
  3. python猜词游戏源代码_Python趣味小游戏编写教学
  4. 数控铣削图案及编程_数控铣加工比普铣的优势,大多数人选择数控铣的原因
  5. html只读下拉框,Html.DropDownList – 禁用/只读
  6. 如何有效开展小组教学_新型教学方法,小组合作教学,有效的提升了学生的合作技能...
  7. 小企业电脑如何组网_(完整版)中小型企业组网方案
  8. java tomcat 日志_java – 访问Tomcat中的详细日志
  9. android自动播放mp3,audio标签移动端(微信)实现自动播放
  10. Win8 Metro(C#)数字图像处理--2.40二值图像轮廓提取算法