一、HashMap简介
HashMap是Java 中的一种基于哈希表实现的集合类,它属于 java.util
包。HashMap
允许存储键值对,并通过键来快速检索值。以下是 HashMap
的一些重要特性和用法:
-
存储结构:
HashMap
使用哈希表数据结构来存储键值对。每个键值对都映射到哈希表中的一个桶,其中每个桶可能包含一个或多个键值对。通过哈希码来确定键值对在桶中的位置,从而实现快速的查找和插入操作。 -
键和值:
HashMap
中的键和值可以是任意类型,包括null
。但需要注意的是,一个HashMap
只能包含一个null
键,但可以包含多个null
值。 -
性能: 由于哈希表的设计,
HashMap
提供了常数时间的平均性能用于插入、删除和查找操作。但在最坏情况下,可能会有冲突(多个键映射到同一个桶),导致性能下降。 -
线程不安全:
HashMap
不是线程安全的,如果多个线程同时访问一个HashMap
实例,且至少有一个线程修改了该HashMap
的结构(如添加或删除元素),则必须在外部进行同步。 -
迭代器:
HashMap
提供了迭代器(Iterator)用于遍历键值对。通过entrySet()
方法获取键值对的集合,然后通过迭代器遍历。 -
初始化容量和负载因子:
HashMap
可以在创建时指定初始容量和负载因子。初始容量是哈希表中桶的数量,负载因子是哈希表在自动扩容之前可以达到的填充因子的浮点值。适当的初始容量和负载因子可以影响HashMap
的性能。
HashMap<String, String> hashmap=new HashMap<String, String>();
hashmap.put("CN","001");
hashmap.put("CN","002");
hashmap.put("BN","003");
hashmap.put("AU","004");
hashmap.put("AU","004");
System.out.println(hashmap);
//输出结果 {AU=004, CN=002, BN=003}
在上述代码中定义了一个HashMap,并向其中添加了一些键值对
(1)当hashmap中已经有键为"CN"的键值对 再向hashmap中添加一个键为"CN",但值不同的元素时,会将原有的"CN"值覆盖
(2)当hashmap中已经有键为"AU"且值为"004"的元素时,再向hashmap中添加一个键为"AU"且值为"004"的元素,HashMap会实现去重
那么HashMap是如何实现去重的呢?我们来看一下HasMap底层的put()方法是如何实现的,首先来了解一下其数据结构
二、底层数据结构
(1)Java中HashMap的实现的基础数据结构是数组,每一对key->value的键值对以双向链表的形式存放到这个数组中
(2)元素在数组中的位置由key.hashCode()的值决定,如果两个key的哈希值相等,即发生了哈希冲突,则这两个key对应的Entity将以链表的形式存放在数组中,即采用链地址法解决冲突
(3)调用HashMap.get()的时候会首先计算key的值,继而在数组中找到key对应的位置,然后遍历该位置上的链表找相应的值。
(4)当冲突发生时,会向计算出来的哈希值对应的存储空间后面添加节点,每添加一个节点都会对数组进行扩容,当数组长度大于等于64并且链表长度大于8时,此时要查找元素很不方便,需要依次向后遍历,所以将链表转为红黑树,树形查找元素会比链表效率高
三、源码解读
- 成员变量定义
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//底层数组初始容量大小:2的4次方 16
static final int MAXIMUM_CAPACITY = 1 << 30;
//数组的最大容量:2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认加载因子 0.75
static final int TREEIFY_THRESHOLD = 8;
//计数阈值 链表长度超过8则转树
static final int UNTREEIFY_THRESHOLD = 6;
//计数阈值 树长度小于6则转链表
static final int MIN_TREEIFY_CAPACITY = 64;
//可对数组进行树化的最小容量
transient Node<K,V>[] table;
//定义Node类型的数组table
transient int size;
//元素个数
transient int modCount;
int threshold;//阈值
final float loadFactor;//扩容的装载因子
- 节点数据结构定义
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; } public final boolean equals(Object o) { if (o == this) return true; return o instanceof Map.Entry<?, ?> e && Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()); } }
- 进入put() 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 可以看到在put方法中,先进行调用了putVal方法,并传入了几个参数,其中第一个参数是计算对于key的一个哈希值,可以看到其计算方法是如果传入key不为0,则用该key的哈希值异或上该key右移16位
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
目的:将key的哈希值高16位和低16位进行异或运算,混合后的哈希值产生冲突的可能性更小
*/
- 进入putVal()方法
inal 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;
// (n - 1) & hash相当于取模,获取数组的索引位置
// 如果计算的位置上Node不存在,直接创建节点插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果计算的位置上Node 存在,链表或者红黑树处理
Node<K,V> e; K k;
// 如果已存在的key和传入的key一模一样,则需要覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果 index 位置元素已经存在,且是红黑树
else if (p instanceof TreeNode)
// 将元素put到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则如果是链表的情况,对链表进行遍历,并统计链表长度
for (int binCount = 0; ; ++binCount) {
// 如果节点链表的next为空
if ((e = p.next) == null) {
// 找到节点链表中next为空的节点,创建新的节点插入
p.next = newNode(hash, key, value, null);
// 如果节点链表中数量超过TREEIFY_THRESHOLD(8)个,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化
treeifyBin(tab, hash);
break;
}
// 判断节点链表中的key和传入的key是否一样
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 如果一样的话,退出
break;
p = e;
}
}
// 如果存在相同key的节点e不为空
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
// 设置新的值
e.value = value;
afterNodeAccess(e);
// 返回旧的结果
return oldValue;
}
}
++modCount;
// 当前大小大于临界大小,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 定义临时变量:Node类型的数组tab,Node类型的节点p,n表示数组的长度,i表示数组的下标
- 如果数组为空,调用resize()扩容数组
- 查找要插入的键值对是否已经存在,如果存在根据条件判断是否用新值代替旧值
- 如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
- 判断键值对数量是否大于阈值,大于则进行扩容操作
- 进入resize()方法
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;//数组现有容量为数组长度 int oldThr = threshold;//此时将现有阈值赋为0 threshold刚开始是0 int newCap, newThr = 0;//新容量 新阈值初始化为0 if (oldCap > 0) { //如果数组现有容量大于0 if (oldCap >= MAXIMUM_CAPACITY) {//如果数组长度超过了定义的最大值 2^30次方 threshold = Integer.MAX_VALUE; return oldTab; //结束扩容 返回现有数组 } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //如果扩大两倍后容量小于最大容量并且数组现有容量大于等于数组默认初始容量16 newThr = oldThr << 1; // double threshold 新的阈值进行两倍扩容 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //如果数组现有容量=0 但是当前扩容阈值大于0,数组新容量变为当前扩容阈值 //如果当前容量等于0,并且当前扩容阈值等于0 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY;//新容量等于默认初始容量16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //新阈值等于加载因子*默认初始容量=0.75*16=12 } //如果新扩容阈值等于0 设置新的扩容阈值等于新的容量*加载因子 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;