目录

  • `HashMap` 概述
  • `jdk 1.8` 之前与之后的 `HashMap`
  • `HashMap` 的数组,链表,红黑树之间的转换
  • `HashMap` 扩容机制
  • `HashMap` 源码
    • `HashMap` 的基本属性
    • `HashMap` 中涉及到的数据结构
      • 链表节点(单链表)
      • 红黑树节点
      • 位桶
    • `HashMap` 的 `put()` 方法
      • 数组判断
      • 红黑树判断
      • 链表判断
    • `HashMap` 的 `get()` 方法
    • `HashMap` 的 `remove` 方法
    • `HashMap` 的 `treeifyBin()` 方法
  • `HashMap` 如何解决 `Hash` 冲突
  • `HashMap` 存入和取出数据顺序不一致
    • 解决办法

HashMap 概述

HashMap 是通过 put(key,value) 存储,get(key)来获取。当传入 key 时,HashMap 会根据 keyhashCode() 方法计算出 hash 值,根据 hash 值将 value 保存在 bucket(桶)里。当计算出的 hash 值相同时,称之为 hash 冲突。HashMap 的做法是用链表和红黑树存储相同 hash 值的 value

jdk 1.8 之前与之后的 HashMap

  • jdk1.8 之前的 HashMap 是由数组 + 链表来实现的,数组是 HashMap 的主体。链表主要是为了解决 hash 冲突的
  • jdk1.8 之后的 HashMap 是由数组 + 链表 + 红黑树来实现的,在解决 hash 冲突时有了较大的变化。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

HashMap 的数组,链表,红黑树之间的转换

  • 当创建 HashMap 集合对象的时候,在 jdk1.8 之前,是在它的构造方法中创建了一个默认长度是 16Entry[] table 的数组来存储键值对数据的。而从 jdk1.8开始,是在第一次调用 put 方法时创建了一个默认长度是 16Node[] table 的数组来存储键值对数据的
  • 数组创建完成后,当添加一个元素(key,value)时,首先计算元素 keyhash 值,以此确定插入数组中的位置。但是可能存在同一 hash 值的元素已经被放在数组同一位置了,这时就添加到同一 hash 值的元素的后面,他们在数组的同一位置,这就形成了单链表,同一各链表上的 Hash 值是相同的。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,这样大大提高了查找的效率
  • 在转换为红黑树存储数据后,如果此时再次删除数据,当红黑树的节点数小于 6 时,那么此时的红黑树将转换为单链表结构来存储数据

HashMap 扩容机制

默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16 * 0.75 = 12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置

0.75 这个值称为负载因子,那么为什么负载因子为 0.75? 这是通过大量实验统计得出来的,如果过小,比如 0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如 1,那么当元素满的时候才进行扩容,会使 get,put 操作的碰撞几率增加

HashMap 源码

HashMap 的基本属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列号private static final long serialVersionUID = 362498820763181265L;    // 默认的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树;+对应的table的最小大小为64,即MIN_TREEIFY_CAPACITY ;这两个条件都满足,会链表会转红黑树static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD = 6;// 桶中结构转化为红黑树对应的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组,总是2的幂次倍transient Node<k,v>[] table; // 存放具体元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transient int size;// 每次扩容和更改map结构的计数器transient int modCount;   // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 填充因子final float loadFactor;
}

HashMap 中涉及到的数据结构

链表节点(单链表)

NodeHashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个单链表的数据结构。链表中的每个节点就是一个 Node 对象

static class Node<k,v> implements Map.Entry<k,v> {final int hash;final K key;V 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;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + = + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为truepublic final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}
}

红黑树节点

红黑树比链表多了四个变量,parent 父节点、left 左节点、right 右节点、prev上一个同级节点

static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {TreeNode<k,v> parent;  // 父节点TreeNode<k,v> left; // 左子树TreeNode<k,v> right;// 右子树TreeNode<k,v> prev; // 删除时需要取消下一个链接boolean red;    // 颜色属性TreeNode(int hash, K key, V val, Node<k,v> next) {super(hash, key, val, next);}// 返回当前节点的根节点final TreeNode<k,v> root() {for (TreeNode<k,v> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}}// ......省略
}

位桶

存储(位桶)的数组

transient Node<K,V>[] table;

HashMap 类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个 Node 的数组

HashMapput() 方法

数组判断

  • 判断 tab[] 数组是否为 null 或长度为 0,如果是 null 或长度为 0;则通过 resize() 方法初始化数组,并获取长度
  • 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,就直接 put 进单链表中,说明此时并没有发生 hash 冲突
  • 如果该数组索引位置之前放入过数据,在通过 keyhash 值,(k = p.key) == key || (key != null && key.equals(k)) 判断该 put 的数据是否与数组索引位置数据相同;如果相同,使用 e = p 来则初始化数组 Node<K,V> e

红黑树判断

  • 如果不相同,则判断是否是红黑树,如果是红黑树就直接插入树中

链表判断

  • 如果不是红黑树,就遍历链表每个节点,并判断链表末尾节点是否为 null;如果为 null,则在该单链表末尾节点插入数据,并判断是否 binCount > 8 -1,为 true 的话会调用 treeifyBin(tab, hash) 方法,该方法在后面详解
  • 然后,判断该 put 的数据是否与单链表的某个节点数据相同,如果相同则跳出循环,执行下一步
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 判断 table[] 是否为空,如果是空的就创建一个 table[],并获取他的长度nif ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;   // 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,// 就直接 put 进单链表中,说明此时并没有发生 Hash 冲突if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// 说明索引位置已经放入过数据了,已经在单链表处产生了Hash冲突Node<K,V> e; K k;// 判断 put 的数据和之前的数据是否重复if (p.hash == hash &&// 进行 key 的 hash 值和 key 的 equals() 和 == 比较,如果都相等,则初始化数组 Node<K,V> e((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 {// 如果不是红黑树,就遍历每个节点,判断单链表长度是否大于等于 7,// 如果单链表长度大于等于 7,数组的长度小于 64 时,会优先选择扩容// 如果单链表长度大于等于 7,数组的长度大于 64 时,才会选择单链表--->红黑树for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {// 采用尾插法,在单链表中插入数据p.next = newNode(hash, key, value, null);// 如果 binCount >= 8 - 1if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash);break;}// 判断索引每个元素的key是否可要插入的key相同,如果相同就直接覆盖if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 说明数组或者单链表中有相同的key,因此只需要将value覆盖,并将oldValue返回即可if (e != null) { V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// 说明没有key相同,因此要插入一个key-value,并记录内部结构变化次数++modCount;// 判断是否扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

HashMapget() 方法

首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找

  1. 判断表或 key 是否是 null,如果是直接返回 null key 对应的 value
  2. 判断索引处第一个 key 与传入 key 是否相等,如果相等直接返回
  3. 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
  4. 如果不是树,就遍历链表查找
public V get(Object key) {Node<K,V> e;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;// 1.如果表不是空的,并且要查找索引处有值,就判断位于第一个的key是否是要查找的keyif ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 1.1.检查要查找的是否是第一个节点if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 1.2.沿着第一个节点继续查找if ((e = first.next) != null) {// 1.2.1.如果是红黑树,那么调用红黑树的方法查找if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 1.2.2.如果是链表,那么采用链表的方式查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

HashMapremove 方法

HashMap 的删除操作并不复杂,仅需三个步骤即可完成

  • 定位桶位置
  • 遍历链表或红黑树并找到键值相等的节点
  • 删除节点
public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {// ------------------1. 查找到要删除的节点------------------                       // tab当前数组,n当前数组容量,p根据hash从数组上找到的节点,index p节点在数组上的位置                      Node<K,V>[] tab; Node<K,V> p; int n, index;// 当数组存在且数组容量大于0,且p节点存在if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;// 当 p 的 hash 等于参数 hash,且 p 的 key 等于参数 key// p节点就是当前要删除的节点,将node指向pif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;// 当p节点不是要删除的节点时,说明p节点上有红黑树或者链表else if ((e = p.next) != null) {// p如果是红黑树,通过getTreeNode()查找节点if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);// p是链表,循环链表查询节点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);}}// ------------------2. 删除查找到的节点------------------// 如果查找到的node存在且machValue为false或v等于valueif (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {// 如果node是TreeNode则使用removeTreeNode删除节点if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);// 如果node等于p,说明移除链表头,将node的后节点放到数组上  else if (node == p)tab[index] = node.next;// 说明移除的不是链表头,根据上方的循环可得,node是p的后节点,将p的后节点指向node的后节点elsep.next = node.next;// 增加修改次数,减少当前数组长度++modCount;--size;afterNodeRemoval(node);return node;}}return null;
}

HashMaptreeifyBin() 方法

当桶中链表长度超过 TREEIFY_THRESHOLD(默认为 8)时,就会调用 treeifyBin() 方法进行树化操作。但此时并不一定会树化,因为在 treeifyBin()方法中还会判断 HashMap 的数组容量是否大于等于 64。如果容量大于等于 64,那么进行树化,否则优先进行扩容

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;/** 如果元素数组为空 或者 数组长度小于 树结构化的最小限制* MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换* 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)* 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。*/if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize(); // 扩容// 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了// 根据hash值和数组长度进行取模运算后,得到链表的首节点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); // 继续遍历链表// 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表// 把转换后的双向链表,替换原来位置上的单向链表if ((tab[index] = hd) != null)hd.treeify(tab);//此处单独解析}
}
  • 如果 tab 数组为 nulltab 的数组长度 < 64 时,调用 resize() 方法
  • 否则,会将链表转换为红黑树(是为了提高查询性能,元素越多,链表的查询性能越差)
  • 说明了链表转换为红黑树的条件:当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

HashMap 如何解决 Hash 冲突

hash 冲突:在 put 多个元素时,通过 keyhashCode() 方法计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了 hash 冲突。当发生 hash 冲突时,会进行如下操作

  • putkey == 已存在的 key 的判断
  • putkey equals() 已存在的 key 的判断(注意 HashMap 中并没有重写 equals() 方法,这里的 equals() 方法仍然是 Object 类的方法)
  • 这里也体现了 ==equals() 方法的判断区别

当上述条件判断,只要有一个返回 false 时,也就是说需要putkey 与已存在的 key 是不相同的,则 HashMap 会使用单链表在已有数据的后面(单链表中)插入新数据,访问的数组下标元素作为链表的头部。这种解决 Hash 冲突的方法被称为拉链法

HashMap 存入和取出数据顺序不一致

HashMap 遍历时,取得数据的顺序是完全随机的,这样会导致按照顺序读取的时候和存入的顺序是不一样的

public class MapTest {public static void main(String[] args) {Map<String, String> map = new HashMap<>();map.put("2020-10-1", "李军");map.put("2020-10-3", "李华");map.put("2020-11-1", "李刚");map.put("2020-10-9", "李奎");for (String key : map.keySet()) {System.out.println(key + ":" + map.get(key));}}
}结果:
2020-10-3 : 李华
2020-11-1 : 李刚
2020-10-1 : 李军
2020-10-9 : 李奎

解决办法

  • 使用 LinkedHashMap
  • 使用 TreeMap:它在内部会对 Key 进行排序
  • 通过有序的 Key 获取相应的数据

Map集合之HashMap相关推荐

  1. 1.5 Map集合:HashMap 和TreeMap 类

    Map集合,HashMap,TreeMap Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象.用于保存具有映射关系的数据 ...

  2. Java基础知识强化之集合框架笔记56:Map集合之HashMap集合(HashMapString,Student)的案例...

    1. HashMap集合(HashMap<String,Student>)的案例 HashMap是最常用的Map集合,它的键值对在存储时要根据键的哈希码来确定值放在哪里. HashMap的 ...

  3. Map集合、 HashMap集合、LinkedHashMap集合、Hashtable集合

    Map集合 Map集合的特点 Map集合是一个双链表结构的集合,一个元素包含两个值(key,value) Map集合中的元素,key和value的数据类型可以相同也可以不同 Map集合中的元素,key ...

  4. 18.集合框架(Map集合,HashMap和Hashtable的区别,Collections(集合工具类),集合练习,模拟斗地主(洗牌,发牌,看牌))

    1.Map集合概述和特点 1.需求:    根据学号获取学生姓名 2.Map接口概述     查看API可以知道:     将键映射到值的对象     一个映射不能包含重复的键     每个键最多只能 ...

  5. Map集合,hashMap的存储过程,Set集合

    1. Map接口 Map接口的特点 map集合的结构是:键值对.KEY与VALUE.Map.Entry<K,V>的映射关系 map中key值不允许重复,如果重复,对应的value会被覆盖 ...

  6. 【Java Map集合 之 hashMap工作常用遍历操作】

    集合关系图 1.文章前介 日常工作中常用的集合有ArrayList.HashMap和HashSet.前两者在开发中更是广为使用.本章主要介绍的是Map下HashMap 在日常工作中的遍历操作.将会以容 ...

  7. Map集合(HashMap,TreeMap)学习总结以及经典案例

    Map: (1)将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值. (2)Map和Collection的区别? A:Map 存储的是键值对形式的元素,键唯一,值可以重复.夫妻对 ...

  8. Map集合(HashMap,TreeMap)

    Map 总想赢者必输,不怕输者必赢 首先对Map进行一个整体的理解. 查看API可以知道,Map 其实就是将键映射到值的对象,每个键最多映射到一个值. Map 与Collection接口的不同: Ma ...

  9. map集合的常用方法和遍历

    概念: 现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射.Java提供了专门的集合类用来存放这种对象关系的对象,即j ...

  10. Java进阶(七)Set系列集合、Map集合体系

    七.Set系列集合.Map集合体系 需要学会什么? Set系列集合的特点:Set系列集合的特点和底层原理. 集合工具类Collections:快速的对集合进行元素的添加.排序等操作. 综合案例:把Co ...

最新文章

  1. python selenium --调用js
  2. 【转】Alert Log Messages: Private Strand Flush Not Complete [ID 372557.1]
  3. niginx高性能原因
  4. r语言 plot_R和Python的特点对比,这样你就知道该怎么选择了
  5. 海量数据处理之Bloom Filter详解
  6. 漫谈C#编程语言在游戏领域的应用
  7. [自学]Docker system 命令 查看docker镜像磁盘占用情况 Docker volume 相关
  8. 【朝夕技术专刊】Core3.1WebApi_Filter-Authorize详解
  9. python计算并返回任意多个整数的和_利用Python的多重处理方法计算一个长输入lin的整数和...
  10. js面向对象的程序设计 --- 上篇(理解对象)
  11. Thread+Handler 线程 消息循环(转载)
  12. HDFS Archival Storage
  13. 分享一款Web压力测试工具Pylot
  14. Unity5.3 使用Awesomium插件内嵌网页
  15. 基于JavaWeb的在线题库管理系统的设计与开发
  16. android 串流 ps4,就想要玩游戏!PS4有线串流到笔记本电脑实战
  17. 富文本编辑器NicEdit的使用
  18. 高德 android 百度转高德,记一次百度和高德经纬度互转(不是你想的那样)
  19. 整理金正昆商务礼仪讲座
  20. win7计算机浏览记录怎么删除,Win7旗舰版系统删除本地浏览记录的技巧

热门文章

  1. 极客大学架构师训练营 微服务网关 领域驱动设计 DDD OAuth 2.0 中台架构 第20课 听课总结
  2. 如何简单访问HTTP的GET、POST、PUT、DELETE,MOCK数据
  3. 用tensorflow实现矩阵分解
  4. Regularization:The problem of overfitting过度拟合问题----吴恩达机器学习
  5. 每日一题/004/矩阵/矩阵问题转化为线性方程组问题
  6. Red-Detector扫描你EC2实例中的安全漏洞
  7. Windows内核研究工具
  8. 刷题记录 kuangbin带你飞专题四:最短路练习
  9. 215.数组中第的K个最大元素(力扣leetcode) 博主可答疑该问题
  10. mysql 查询最大值的总和_mysql中最大值和最小值以及总和查询与计数查询的实例详解...