何为HashMap:HashMap是常用的数据结构,由数组和链表组合构成,可理解为链表构成的数组(注:jdk1.8之后如果链表长度达到阈值(默认为8),就会更改使用红黑树)

1.红黑树相关关键参数

//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;想知道为何为8?//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素还原为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;//当多个bin被映射到同一个桶时,如果这个桶中bin的数量小于TREEIFY_THRESHOLD(8)当然不会转化成树形结构存储;如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD(8) ,但是capacity小于MIN_TREEIFY_CAPACITY (64)则依然使用链表结构进行存储,此时会对HashMap进行扩容;如果capacity大于了MIN_TREEIFY_CAPACITY (64),则会进行树化。

这里为何NTREEIFY_THRESHOLD = 6,而TREEIFY_THRESHOLD = 8,也是有依据的。

2.桶的树形化 treeifyBin()

 /*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.*///将桶内所有的链表节点替换成红黑树节点final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;//若当前哈希表为空,或者数组table的长度小于MIN_TREEIFY_CAPACITY时,进行resize(),//这里需要注意两点:1.resize()中包括初始赋值,也包括扩容//               2.从源码中可以看出,hashMap不是有一个链表达到TREEIFY_THRESHOLD就会马上进行红黑树转换,在执行树形化之前,会先检查数组长度,如果长度小于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);}}TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}

table发生扩容的时候有两种情况:

一种是元素达到阀值了,即(Capacity:HashMap当前长度*LoadFactor:负载因子,默认值0.75f)。
一种是HashMap准备树形化但又发现数组太短(小于MIN_TREEIFY_CAPACITY),这两种情况均可能发生扩容。这种情况树形化其实是治标不治本。

3.红黑树中添加元素 putTreeVal()

在添加时,如果一个桶中已经是红黑树结构,就要调用红黑树的添加元素方法 putTreeVal()。

4. 红黑树中查找元素 getTreeNode()

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5.树形结构修剪 split()

HashMap 中, resize() 方法的作用就是初始化或者扩容哈希表。当扩容时,如果当前桶中元素结构是红黑树,并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6),就会把桶中的树形结构缩小或者直接还原(切分)为链表结构,调用的就是 split():

6.jdk1.7的头插法(Entry)和jdk1.8(Node)的尾插法

hashmap在java8之前是头插法:新来的值会插入到旧值的前面,新值的内存地址成为tableb表的值,原有的值就顺推到链表中去。java8之后使用的是尾插法:新来的值插到链表后面。但是头插法在并发的情况下,会引发死循环。因为我们都知道,hashmap再达到一定的容量时,会引起HashMap容器的扩容resize(),扩容后就会rehash(),为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变, hash公式:index = HashCode(Key) & (Length - 1)。而就是再rehash这个过程中,如果有多个并发线程进行rehash,就有可能造成rehash后的链表中存在环,(形成环的根本原因:就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上,如下)



当下一次要在这个链表中找一个不存在的值时,就会形成死循环,所以jdk1.8之后就使用了尾插法,但是,即使是这样,hashmap还是线程不安全的,如果要解决这个问题,考虑ConcurrentHashMap.这篇大佬的文章对JDK1.7Rehash()形成环的过程讲得较明白(个人认为)而JDK1.8之后,链表有红黑树的部分,但链表过程时,转换成红黑树,将原本O(n)的时间复杂度降低到了O(logn)。JDK1.8采取的尾插法在扩容时会保持链表元素原本的顺序,不会出现链表成环的问题,原因是扩容转移后前后链表顺序不变(JDK1.7扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。)

7.hashmap的默认初始化长度:16

默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16),这样做的最终目的就是:只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的,可以减少哈希碰撞,实现均匀分布。同时位运算是直接在内存中进行的,效率比取模运算高太多了
原代码中有如下:

//该方法原理就是返回元素所在的桶的位置
static int indexFor(int h, int length) {return h & (length-1);
}

原理:X % 2^n = X & (2^n – 1)
如下例子:

6 % 8 = 6 ,6 & 7 = 6
10 % 8 = 2 ,10 & 7 = 2


这篇文章写得太好了

8.JDK1.8 putVal()方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。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 {//如果是个链表,就需要将当前的 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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//最后判断是否需要进行扩容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

9.JDK1.8 get()方法

public V get(Object key) {Node<K,V> e;//首先将 key hash 之后取得所定位的桶,如果桶为空则直接返回 null 。return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) {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) {//否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;//如果第一个不匹配,则判断它的下一个是否为空,若不为空,再判断是红黑树还是链表。if ((e = first.next) != null) {//红黑树就按照树的查找方式返回值。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;} while ((e = e.next) != null);}}return null;}

8.解决hashmap的线程不安全

使用hashTable或者ConcurrentHashMap,但是因为hashtable并发度的原因基本上没啥使用场景了(hashtable的解决方法比较粗暴,直接在方法上锁,并发度很低,最多同时允许一个线程访问),所以主流都是使用ConcurrentHashMap。

HashMap(Java)相关推荐

  1. 一些面试题(JAVA)

    一些面试题(JAVA) 01.通过什么参数分配Java内存使用? java -Xms128m -Xmx512m 02.Treemap和Hashmap区别是什么? TreeMap对Key进行排序,而Ha ...

  2. 面试再也不怕问到HashMap(二)

    详细源码解析查看面试再也不怕问到HashMap(一) HashMap干货总结 1,可以用null做为key和value 2,capacity和load factor影响性能 3,线程不安全 4,Has ...

  3. 笔试编程常用函数(Java)

    笔试编程常用函数(Java) 处理对象为String 数组相关 集合相关 (今天太晚了,以后再慢慢写) 处理对象为String 将String对象按某规则分割: String[] split(Stri ...

  4. 时空权衡在模式匹配算法中的应用(JAVA)--Horspool算法(简化版BM算法)

    模式匹配是数据结构中字符串的一种基本运算,给定一个子串,要求在某个字符串中找出与该子串相同的所有子串.假设P是给定的子串,T是待查找的字符串,要求从T中找出与P相同的所有子串,这个问题成为模式匹配问题 ...

  5. HashMap(HashSet)的实现

    0. HashMap(TreeMAP).HashSet.HashTable 的关系 HashMap 的底层则维护着 Node<K, V>[] table; 一个一维数组用于快速访问(只在初 ...

  6. 根据经纬度计算指定范围内或者附近的人(java)

    文章目录 根据经纬度计算指定范围内或者附近的人(java) 前言 实现 根据经纬度计算指定范围内或者附近的人(java) 前言 最近项目中有一个需求,就是在地图中随便标一个点位,然后根据这个点位显示出 ...

  7. 赵栋/东文才 《面向对象程序设计(java)》第十一周学习总结

    201771010137 赵栋<面向对象程序设计(java)>第十一周学习总结 一:理论部分. 1.数据结构:分为a.线性数据结构,如线性表.栈.队列.串.数组和文件. b.非线性数据结构 ...

  8. 东文财、赵栋《面向对象程序设计(java)》第十一周学习总结

    201771010106 东文财<面向对象程序设计(java)>第十一周 实验时间 2018-11-8 一:理论部分. 1.数据结构: a.线性数据结构,如线性表.栈.队列.串.数组和文件 ...

  9. 微信公众账号第三方平台全网发布源码(java)- 实战测试通过

    微信公众账号第三方平台全网发布源码(java)- 实战测试通过 (更多资料,关注论坛:www.jeecg.org) 技术交流请加:289709451.287090836 package org.jee ...

最新文章

  1. pdo mysql防注入_Php中用PDO查询Mysql来避免SQL注入风险的方法
  2. [Unity UGUI]点击和长按组件
  3. Spring Boot2.0之 整合Redis集群
  4. Kitten编程猫的工程文件 bcm,能发布成Android平台的apk文件吗
  5. 对称密码和非对称密码体系_密码学类型:对称和不对称
  6. arm-linux-gcc 和Makefile
  7. @程序员,与JavaScript 疲劳症SayBye!
  8. python解释器的提示符是shell嘛_windows python3.2 shell环境(python叫做解释器)
  9. Everything使用攻略和技巧
  10. 亚马逊AWSome DAY
  11. python seo编程_gogo闯SEO快排教程及应用编程(同步官方)
  12. ITU-R BT.601 Y'CbCr
  13. 相约上海,百家公司齐聚SDCC 2017·上海站,约吗?(附名单及参会提醒)
  14. 西班牙符号Ññ 乱码的问题
  15. 福禄克用CFP-Q-ADD实现光纤一级认证测试
  16. .关机助手,很好用的Windows桌面关机小工具(免费版)
  17. 禁止服务器的协议,电脑怎么阻止Internet控制消息协议ICMP?
  18. 【上海交大复试失利总结】横向纵横 动态规划
  19. Houdini脚本Vex学习
  20. 【论文笔记_知识蒸馏_2022】Masked Generative Distillation

热门文章

  1. 博客使用的CSS代码备份
  2. 遇到的Ajax相关问题
  3. oracle--rowid
  4. 时光已荏苒,我还怎么让你遇见最美年华里的我
  5. DB Query Analyzer 中断SQL语句的执行
  6. XPath函数——字符串函数
  7. Python学习笔记:偏函数
  8. Linux服务器与windows本地之间的数据同步
  9. oracle 一行转多行,oracle 一行变多行
  10. ImportError: No module named sklearn.metrics