此文章用来记录hashmap的一些特点(在学习中的所了解的,如有不足,请指正)

什么是hash表

概念

先来一段百度百科的的解释

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

所谓的hash表在我看来嘛就是映射嘛,以前嘛要查找一个数或者一个值嘛是通过遍历的形式,这样的话就会有一个问题,那就是太浪费时间了,时间效率非常低,也不能非常低嘛,时间复杂度是O(n)。于是呢,人们为了更快的找到需要查找的值呢就想到了一种办法,将存储的位置与存储的值对应起来,这样查找的效率不就高了很多。但是怎么转换呢,聪明的人类想到了一种办法,利用一种函数映射的形式来解决,这个映射用的函数就叫做hash函数,这个表呢就叫hash散列表,但是呢这是有问题的。那就是

hash表冲突

很好理解嘛,不同的值可能经过hash函数生成同样的索引,这样的话就有冲突了,怎么解决?请看

hash表冲突的解决

我所了解的常用的

  • 直接寻址,也叫开放地址法,就是这个不能放我不放了,我放到下一个去,要是下一个还有就继续往后直到找到可以插入的位置,要是都没有,那就考虑一下扩容呗
  • hash再散列,就是用别的hash算法再算一遍
  • 拉链法,这个方法就是hashmap中用到的方法。不是有冲突嘛,统统拿来,统统放这,一个别想跑。其实就是利用链表,冲突了就追加节点(不是同一个的话才追加)
  • 建立公共溢出区,就是冲突了嘛,没坑了,那就走吧,不要呆在这里了

以上就是我所了解的,估计也是常用的吧,不然我也不会了解

HashMap

map的意思嘛,就是映射,才不是地图。Java中的HashMap就是利用hash表加链表实现的K,V形式的数据结构,和python中的字典是一样的。hashmap中的hash冲突的解决利用的是拉链法。1.7之前的拉链是只有链表,而在1.8增加了一个红黑树结构,这是因为,当链表长度太长的时候查找效率比较低。所以在hash桶数据的容量大于等于64以及hash桶内的元素数量大于8时就会转换为红黑树。有人说是大于等于8,大于64的时候,今天我们进入源码一探究竟,先来看个静态常量

static final int MIN_TREEIFY_CAPACITY = 64;

树化:treeifyBin

final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果说table 为null或者说容量小于64就扩容,不执行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 如果说 所在的位置表不为空
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

上面的就是进行树化的操作了,那么怎么才能树化呢

扩容:resize()方法

先准备一个重要的方法,resize()方法

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//判断老表是否为null为null的话长度就是0
int oldThr = threshold; // 保存原来的老阈值
int newCap, newThr = 0; //先将新表的长度 阈值设置为0
if (oldCap > 0) {
//如果说老表的容量大于0且容量大于等于最大容量(MAXIMUM_CAPACITY = 1 << 30)
//就将阈值设为Integer.MAX_VALUE,然后直接返回也就是不再扩容了,仅仅将阈值增大就行了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果说老容量乘以2小鱼最大容量以及大于等于默认的容量( DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16)
// 就将原来的阈值也扩充为两倍 就是说这里没啥意外容量就定下来了,也是一般的扩容情况
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果说老表的容量小于等于0,但是老阈值大于0,就将新的容量设置为老阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果老表的容量以及老阈值都不大于0,就执行初始操作,将新表的容量设置为16,计算新表的阈值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果说得出的新表阈值等于0的话就用新表的容量乘以负载因子,然后如果说新表的容量小于最大值以及新的阈值小于最大值,就将新阈值设为所求,否则就是Integer.MAX_VALUE
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
MAX_VALUE (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循环
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;
//如果说e是treeNode节点,也就是说,这个hash桶里边的节点已经树化过了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//下面这个else的意思是如果有下一个节点而且没有树化,也是说是链表形式的至少有两个节点
else { // preserve order
//这里就是将链表分成两种一种是高位链表一种是低位链表,至于什么是高低位链表,咱们往下看
//loHead低位头节点
//loTail低位尾节点
Node<K,V> loHead = null, loTail = null;
//hiHead高位头节点
//hiTail低位尾节点
Node<K,V> hiHead = null, hiTail = null;
//next节点
Node<K,V> next;
//开始do..While循环,因为肯定有next节点嘛,不然也到不了这里
do {
//保存一下next节点
next = e.next;
//注意这个与的 是与原来的容量进行比较的没有进行减一哈,减一是求索引用的。
//这里的意思举个例子来说就是比如原来的容量就是16吧,因为这里是位运算嘛,转换成二进制就是10000
//因为这里是等于0 的情况嘛,所以就假设e.hash二进制为1011001111吧,索引算出来就是1111
//运算开始 因不足用0补齐嘛
//1011001111
// 10000
//----------
//0000000000
//嗯 就是这种情况,因为原来容量二进制是5位也就是说如果hash值第五位是0,那么就扩容以后不会有任何变化
//因为扩容是变为原来的2倍,也就是左移一位变为100000。
//那么减1以后就是11111,刨去后边的4个1,两个最高位都是1也就是相同的,可以直接运算
//如果说此时元素的hash值在这个最高位是0的话,那么算出的索引与原来是一样的,这也就是低位索引
//这里只是将低位放在一起
if ((e.hash & oldCap) == 0) {
//如果尾节点为空就初始化(说明头节点也没值)
if (loTail == null)
//这里的头节点指示头所在的位置,以后追加就是用为节点了,高位链表一样如此
loHead = e;
else
//让尾节点的next指向e,
loTail.next = e;
//然后尾节点向后移一位
//这里写成loTail=loTail.next我感觉比较好理解一些
loTail = e;
}
//如果说不是0的话,说明hash值的高位是1,经过运算后就是11111就是原来的索引加上2^4
//就是原来的表的长度,所以高位链表只需要原来的索引加上原来的表的长度就是新的索引
//这里只是将高位放到一起
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//循环结束后,如果说低位链表不为空的话就说明执行了分高低位的工作,而且有低位的存在
//然后只需要将hash桶的节点指向低位链表的头节点,而且因为是低位链表嘛,索引跟原来的一样
if (loTail != null) {
//这里将为节点的next设置为null,因为这再遍历的时候尾节点的next与尾节点指向同一个位置
//因为已经遍历完了嘛,next也就没有值了,所以就清空。高位链表类似
loTail.next = null;
newTab[j] = loHead;
}
//这里判断高位链表是否为空,空的就说明没有高位链表嘛
//不空的话就将原来的索引加上老表的容量,至于为什么,上面已经解释过
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//至此返回新的链表
return newTab;
}

你以为到这里就结束了?no,no,no,还有一个重要的方法,那就是如果是红黑树的话,怎么进行操作呢,就是这个红黑树节点里边的split(this, newTab, j, oldCap)方法了

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index,
int bit//这个bit什么意思呢,我猜是老表的容量,上面传过来的oldCap
) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//开头雷击,梦回链表
//这怎么和链表的操作差不多呢,也是分成高低位
//其实注释上面也写了嘛
//将树箱中的节点拆分为较高和较低的树箱,如果现在太小,则取消树化。
//为什么红黑树的节点也可以这样呢,因为
/**
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
// needed to unlink next upon deletion
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
*/
//所以有个成员变量是next,那不就跟链表一样了
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//这个lc吧 low count:低位的数量
//hc呢 high count:高位的数量
int lc = 0, hc = 0;
//显而易见,这里遍历treeNode
//这里为什么不用while循环呢,是不用在外面声明变量嘛
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//这里高低位一分,对应的count++就不展开细说了
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
} if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
//如果说数量小于等于UNTREEIFY_THRESHOLD=6,就弄成链表
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
//如果不小于6且高位链表不为null就树化
//为啥需要高位链表不为空呢,
/**
这里个人理解,高位链表如果为空,说明旧数组下的红黑树中的元素在新数组中仍然全部在同一个位置,
且先后顺序没有改变,也就是注释中的已经树化了,没有必要再次树化;而当高位节点不为空,
说明原链表元素被拆分了,且位红黑树节点个数大于6,不满足转链表条件,需要重新树化。
此处来自https://blog.csdn.net/hengwu1817/article/details/107095871/
*/
//下面的高位链表也是如此
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}

插入数据 put

调用put(k,v)方法实际上调用的是putVal方法

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

所以只需要分析putVal方法即可

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,//是否不存在才插入
boolean evict//文档给的是创建表的模式,我的理解是可读可写
) {
Node<K,V>[] tab;
Node<K,V> p; //p是table[i]所在的头
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//如果说原来的表不存在或者为空就执行resize()方法,上面已经进入看了一下
//如果说原来的表的位置等于空的话就直接放进去 不存在冲突
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//这里才是重点 到这里说明发生冲突
Node<K,V> e;//e表示要要插入的节点
K k;
//这个判断是看原来的老的hash值跟我传进来的hash值是否相同并且key也相同 或者说key不为空并且相同
//也就是判断一下是不是相同的key 是的话就将p赋值给e
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);
//如果说大于等于8-1也就是7的话就树化 因为要插入元素了嘛,所以插入以后就等于8了
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这里key相同的话就中断
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//找到了要插入的节点后 如果e不为null 说明key是一样的 只需要替换一下值就好了
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;
}

对于1.7的hashmap的死循环问题以及本篇文章的1.8的死循环数据覆盖问题,以后再总结

HashMap稍微详细的理解的更多相关文章

  1. 稍微深入点理解C++复制控制【转】

    通过一个实例稍微深入理解C++复制控制过程,参考资料<C++ primer>,介绍点基本知识: 1.在C++中类通过特殊的成员函数:复制构造函数.赋值操作符和析构函数来控制复制.赋值和撤销 ...

  2. HashMap和HashTable的理解与区别

    Hashtable是java一开始发布时就提供的键值映射的数据结构,而HashMap产生于JDK1.2.虽然Hashtable比HashMap出现的早一些,但是现在Hashtable基本上已经被弃用了 ...

  3. Hash算法及java HashMap底层实现原理理解(含jdk 1.7以及jdk 1.8)

    现在很多公司面试都喜欢问java的HashMap原理,特在此整理相关原理及实现,主要还是因为很多开发集合框架都不甚理解,更不要说各种其他数据结构了,所以造成面子造飞机,进去拧螺丝. 1.哈希表结构的优 ...

  4. JDK源码分析之hashmap就这么简单理解

    一.HashMap概述 HashMap是基于哈希表的Map接口实现,此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap与HashTable的作用大致相同,但是它不是线程安全 ...

  5. HashMap与HashTable的理解与区别

    Hashtable是java一开始发布时就提供的键值映射的数据结构,而HashMap产生于JDK1.2.虽然Hashtable比HashMap出现的早一些,但是现在Hashtable基本上已经被弃用了 ...

  6. hashMap 源码解读理解实现原理和hash冲突

    hashMap 怎么说呢. 我的理解是 外表是一个set 数组,无序不重复 . 每个set元素是一个bean ,存着一对key value 看看代码吧 package test; import jav ...

  7. 使用XML定制Ribbon的一点小前奏(稍微再进一步的理解XML)

    定制文档级Ribbon界面的实现思路: 1.excel的文件使用rar+xml的形式保存在本地. 2.用压缩软件打开文件,以规范的格式直接编缉或添加xml文件 3.使用excel文件时,主程序会解析x ...

  8. HashMap(常用)方法个人理解

      Hashmap的存值: public static void main(String[] args) { ///*Integer*/map.put("1", 1);//向map ...

  9. HashMap resize方法的理解(一)

    对于oldTable中存储的为15.7.4.5.8.1,长度为8的一个数组中,存储位置如下 0 1 2 3 4 5 6 7 8 1 4 5 15 7 当扩容到一倍后,对于新的位置的选择通过e.hash ...

随机推荐

  1. Sublime Text 3 安装插件与快捷键总结

    ublime Text 3 是一个了不起的软件.首先,它是一个干净,实用,可以快速的编写代码编辑器.它不仅具有令人难以置信的内置功能(多行编辑和VIM模式),而且还支持插件,代码片段和其他许多东西.很 ...

  2. 《Machine Learning in Action》—— 剖析支持向量机,优化SMO

    <Machine Learning in Action>-- 剖析支持向量机,优化SMO 薄雾浓云愁永昼,瑞脑销金兽. 愁的很,上次不是更新了一篇关于支持向量机的文章嘛,<Machi ...

  3. python之路 《四》 字典

    python中的字典使得python来解决问题变得更方便,顾名思义,就是当我知道关键字(key)那么我就可以通过key来找到与之对应的信息. 简单的来说字典是另一种可变容器模型,且可存储任意类型对象. ...

  4. 手写一个最迷你的Web服务器

    今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码.  2. 代码介绍: ( ...

  5. ubuntu服务器dns重启失效问题

    方法一 通过/etc/network/interfaces,在它的最后增加一句: dns-nameservers 8.8.8.8 8.8.8.8是Google提供的DNS服务,这里只是举一个例子,你也 ...

  6. RESTFul服务开发必备的一款IDEA插件!用了就离不开了

    我们经常谈 RESTful Web 服务开发,但是我发现很多人实际就根本不懂这个概念.只是听着大家都这么说,也就跟着一起说了,哈哈哈! 因此,在开始推荐这个IDEA插件之前,非常有必要花一小会时间简单 ...

  7. js,昨天的日期

    var mydate = new Date(); var yester = mydate-24*60*60*1000; var yesterday = new Date(); yesterday.se ...

  8. HDU100题简要题解(2070~2079)

    HDU2070 Fibbonacci Number 题目链接 Problem Description Your objective for this question is to develop a ...

  9. ssh 方面问题总结

    ssh 远程执行命令: https://www.cnblogs.com/youngerger/p/9104144.html ssh免密登录: https://blog.csdn.net/jeikerx ...

  10. 关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白

    前言 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间).进程不依赖于线程而独立存在,一个进程中可以启动多个线程. 线程是指进程中的一个执行流程,一个进程中可 ...