Python 官方文档:入门教程 => 点击学习
目录HashMap 概述jdk 1.8 之前与之后的 HashMapHashMap 的数组,链表,红黑树之间的转换HashMap 扩容机制HashMap 源码HashMap 的基本属
HashMap 是通过 put(key,value) 存储,get(key)来获取。当传入 key 时,HashMap 会根据 key 的 hashCode() 方法计算出 hash 值,根据 hash 值将 value 保存在 bucket(桶)里。当计算出的 hash 值相同时,称之为 hash 冲突。HashMap 的做法是用链表和红黑树存储相同 hash 值的 value
默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16 * 0.75 = 12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置
0.75 这个值称为负载因子,那么为什么负载因子为 0.75 呢? 这是通过大量实验统计得出来的,如果过小,比如 0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如 1,那么当元素满的时候才进行扩容,会使 get,put 操作的碰撞几率增加
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static 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;
}
链表节点(单链表)
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。可以与自身比较为true
public 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 的数组
数组判断
红黑树判断
链表判断
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) {
// tab表示 Node<K,V>类型的数组,p表示某一个具体的单链表 Node<K,V> 节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断 table[] 是否为空,如果是空的就创建一个 table[],并获取他的长度n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// tab[i = (n - 1) & hash] 表示数组中的某一个具体位置的数据
// 如果单链表 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 - 1
if (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;
}
}
if (e != null) {
// 此时说明 key 的 hash 值和 key 的 equals() 和 == 比较结果都相等
// 说明数组或者单链表中有完全相同的 key
// 只需要将 value 覆盖,并将 oldValue 返回即可
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;
}
首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找
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是否是要查找的key
if ((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 的删除操作并不复杂,仅需三个步骤即可完成
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指向p
if (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等于value
if (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的后节点
else
p.next = node.next;
// 增加修改次数,减少当前数组长度
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
当桶中链表长度超过 TREEIFY_THRESHOLD(默认为 8)时,就会调用 treeifyBin() 方法进行树化操作。
但此时并不一定会树化,因为在 treeifyBin()方法中还会判断 HashMap 的数组容量是否大于等于 64。
如果容量大于等于 64,那么进行树化,否则优先进行扩容
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
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);//此处单独解析
}
}
hash 冲突:在 put 多个元素时,通过 key 的 hashCode() 方法计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了 hash 冲突。当发生 hash 冲突时,会进行如下操作
当上述条件判断,只要有一个返回 false 时,也就是说需要put 的 key 与已存在的 key 是不相同的,则 HashMap 会使用单链表在已有数据的后面(单链表中)插入新数据,访问的数组下标元素作为链表的头部。这种解决 Hash 冲突的方法被称为拉链法
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 : 李奎
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。
--结束END--
本文标题: Map集合之HashMap的使用及说明
本文链接: https://www.lsjlt.com/news/169263.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0