Map集合之HashMap
目录
- `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
会根据 key
的 hashCode()
方法计算出 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
之前,是在它的构造方法中创建了一个默认长度是16
的Entry[] table
的数组来存储键值对数据的。而从jdk1.8
开始,是在第一次调用put
方法时创建了一个默认长度是16
的Node[] table
的数组来存储键值对数据的 - 数组创建完成后,当添加一个元素
(key,value)
时,首先计算元素key
的hash
值,以此确定插入数组中的位置。但是可能存在同一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
中涉及到的数据结构
链表节点(单链表)
Node
是 HashMap
的一个内部类,实现了 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
的数组
HashMap
的 put()
方法
数组判断
- 判断
tab[]
数组是否为null
或长度为0
,如果是null
或长度为0
;则通过resize()
方法初始化数组,并获取长度 - 如果单链表
Node<K,V> p == tab[i = (n - 1) & hash]) == null,
就直接put
进单链表中,说明此时并没有发生hash
冲突 - 如果该数组索引位置之前放入过数据,在通过
key
的hash
值,(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;
}
HashMap
的 get()
方法
首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找
- 判断表或
key
是否是null
,如果是直接返回null key
对应的value
- 判断索引处第一个
key
与传入key
是否相等,如果相等直接返回 - 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
- 如果不是树,就遍历链表查找
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;
}
HashMap
的 remove
方法
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;
}
HashMap
的 treeifyBin()
方法
当桶中链表长度超过 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
数组为null
或tab
的数组长度< 64
时,调用resize()
方法 - 否则,会将链表转换为红黑树(是为了提高查询性能,元素越多,链表的查询性能越差)
- 说明了链表转换为红黑树的条件:当链表长度大于阈值
8
时,并且数组的长度大于64
时,此时此索引位置上的所有数据改为使用红黑树存储
HashMap
如何解决 Hash
冲突
hash
冲突:在 put
多个元素时,通过 key
的 hashCode()
方法计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了 hash
冲突。当发生 hash
冲突时,会进行如下操作
put
的key
==
已存在的key
的判断put
的key
equals()
已存在的key
的判断(注意HashMap
中并没有重写equals()
方法,这里的equals()
方法仍然是Object
类的方法)- 这里也体现了
==
与equals()
方法的判断区别
当上述条件判断,只要有一个返回 false
时,也就是说需要put
的 key
与已存在的 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.5 Map集合:HashMap 和TreeMap 类
Map集合,HashMap,TreeMap Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象.用于保存具有映射关系的数据 ...
- Java基础知识强化之集合框架笔记56:Map集合之HashMap集合(HashMapString,Student)的案例...
1. HashMap集合(HashMap<String,Student>)的案例 HashMap是最常用的Map集合,它的键值对在存储时要根据键的哈希码来确定值放在哪里. HashMap的 ...
- Map集合、 HashMap集合、LinkedHashMap集合、Hashtable集合
Map集合 Map集合的特点 Map集合是一个双链表结构的集合,一个元素包含两个值(key,value) Map集合中的元素,key和value的数据类型可以相同也可以不同 Map集合中的元素,key ...
- 18.集合框架(Map集合,HashMap和Hashtable的区别,Collections(集合工具类),集合练习,模拟斗地主(洗牌,发牌,看牌))
1.Map集合概述和特点 1.需求: 根据学号获取学生姓名 2.Map接口概述 查看API可以知道: 将键映射到值的对象 一个映射不能包含重复的键 每个键最多只能 ...
- Map集合,hashMap的存储过程,Set集合
1. Map接口 Map接口的特点 map集合的结构是:键值对.KEY与VALUE.Map.Entry<K,V>的映射关系 map中key值不允许重复,如果重复,对应的value会被覆盖 ...
- 【Java Map集合 之 hashMap工作常用遍历操作】
集合关系图 1.文章前介 日常工作中常用的集合有ArrayList.HashMap和HashSet.前两者在开发中更是广为使用.本章主要介绍的是Map下HashMap 在日常工作中的遍历操作.将会以容 ...
- Map集合(HashMap,TreeMap)学习总结以及经典案例
Map: (1)将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值. (2)Map和Collection的区别? A:Map 存储的是键值对形式的元素,键唯一,值可以重复.夫妻对 ...
- Map集合(HashMap,TreeMap)
Map 总想赢者必输,不怕输者必赢 首先对Map进行一个整体的理解. 查看API可以知道,Map 其实就是将键映射到值的对象,每个键最多映射到一个值. Map 与Collection接口的不同: Ma ...
- map集合的常用方法和遍历
概念: 现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射.Java提供了专门的集合类用来存放这种对象关系的对象,即j ...
- Java进阶(七)Set系列集合、Map集合体系
七.Set系列集合.Map集合体系 需要学会什么? Set系列集合的特点:Set系列集合的特点和底层原理. 集合工具类Collections:快速的对集合进行元素的添加.排序等操作. 综合案例:把Co ...
最新文章
- python selenium --调用js
- 【转】Alert Log Messages: Private Strand Flush Not Complete [ID 372557.1]
- niginx高性能原因
- r语言 plot_R和Python的特点对比,这样你就知道该怎么选择了
- 海量数据处理之Bloom Filter详解
- 漫谈C#编程语言在游戏领域的应用
- [自学]Docker system 命令 查看docker镜像磁盘占用情况 Docker volume 相关
- 【朝夕技术专刊】Core3.1WebApi_Filter-Authorize详解
- python计算并返回任意多个整数的和_利用Python的多重处理方法计算一个长输入lin的整数和...
- js面向对象的程序设计 --- 上篇(理解对象)
- Thread+Handler 线程 消息循环(转载)
- HDFS Archival Storage
- 分享一款Web压力测试工具Pylot
- Unity5.3 使用Awesomium插件内嵌网页
- 基于JavaWeb的在线题库管理系统的设计与开发
- android 串流 ps4,就想要玩游戏!PS4有线串流到笔记本电脑实战
- 富文本编辑器NicEdit的使用
- 高德 android 百度转高德,记一次百度和高德经纬度互转(不是你想的那样)
- 整理金正昆商务礼仪讲座
- win7计算机浏览记录怎么删除,Win7旗舰版系统删除本地浏览记录的技巧
热门文章
- 极客大学架构师训练营 微服务网关 领域驱动设计 DDD OAuth 2.0 中台架构 第20课 听课总结
- 如何简单访问HTTP的GET、POST、PUT、DELETE,MOCK数据
- 用tensorflow实现矩阵分解
- Regularization:The problem of overfitting过度拟合问题----吴恩达机器学习
- 每日一题/004/矩阵/矩阵问题转化为线性方程组问题
- Red-Detector扫描你EC2实例中的安全漏洞
- Windows内核研究工具
- 刷题记录 kuangbin带你飞专题四:最短路练习
- 215.数组中第的K个最大元素(力扣leetcode) 博主可答疑该问题
- mysql 查询最大值的总和_mysql中最大值和最小值以及总和查询与计数查询的实例详解...