上图是Map的大致继承关系图,集合其实就是一个容器,下面我主要以增、删、改查和遍历来讲解接口和具体的实现类。
首先来看Map接口:
一、最顶层Map接口
public interface Map<K,V> {
/*键值对接口 Entry*/
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
//最重要的三个方法
V get(Object key);//根据key,获取value
V put(K key, V value);//添加键值对
V remove(Object key);//删除指定的键值对
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
void putAll(Map<? extends K, ? extends V> m);
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
}
增: V put(K key, V value);
删:V remove(Object key);
改:是否替换
查:V get(Object key);
遍历:Set keySet(); Set<Map.Entry<K, V>> entrySet();
二、SortedMap接口
public interface SortedMap<K,V> extends Map<K,V> {
//因为是有序的,所有可以根据key的范围生成子map
SortedMap<K,V> subMap(K fromKey, K toKey);
K firstKey();//第一个key
K lastKey();//最后一个key
}
三、NavigableMap接口
public interface NavigableMap<K, V> extends SortedMap<K, V> {
Entry<K, V> lowerEntry(K var1);//严格比他小的
K lowerKey(K var1);
Entry<K, V> floorEntry(K var1);//不大于给定key的 最大值
K floorKey(K var1);
Entry<K, V> ceilingEntry(K var1);//不小于给定key的 最小的值
K ceilingKey(K var1);
Entry<K, V> higherEntry(K var1);//严格比他大的
K higherKey(K var1);
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();//获取第一个,并移除
Entry<K, V> pollLastEntry();//获取最后一个,并移除
NavigableMap<K, V> descendingMap();
NavigableSet<K> navigableKeySet();
NavigableSet<K> descendingKeySet();
NavigableMap<K, V> subMap(K var1, boolean var2, K var3, boolean var4);
NavigableMap<K, V> headMap(K var1, boolean var2);
NavigableMap<K, V> tailMap(K var1, boolean var2);
}
实现类
一、HashMap详解
主要通过以下四个方法来详解:
增: V put(K key, V value);
删:V remove(Object key);
改:是否替换
查:V get(Object key);
遍历:Set keySet(); Set<Map.Entry<K, V>> entrySet();
1.解决hash冲突的方法有哪些?
- 开放定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式: - 链地址法(常用)
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
2.HashMap源码解析
HashMap的特点:
①HashMap底层
JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的“数组+链表”改为“数组+链表+红黑树”。当链表长度大于8 的时候会转换成红黑二叉树。
②成员变量的解释
//常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 底层数据初始大小 16 且容量必须是2的幂次方
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//装载因子 默认0.75f
static final int TREEIFY_THRESHOLD = 8;//链表长度大于此数 就转成红黑二叉树
static final int UNTREEIFY_THRESHOLD = 6;//小于此数转为链表
static final int MIN_TREEIFY_CAPACITY = 64;//链表转红黑二叉树 最小容量64 否则就扩容
//成员变量
transient Node<K,V>[] table; //数组
transient Set<Map.Entry<K,V>> entrySet;
transient int size;//size
transient int modCount;//用于迭代器抛异常 快速失败
int threshold;//容量大小 容量*装载因子 当size大于此数就会扩容
final float loadFactor;
③构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor() 此函数将容量变成2的幂次方 比如传入5变成8
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Returns a power of two size for the given target capacity.
* 为什么要变成2的幂次方呢?
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
为什么要变成2的幂次方呢?要搞懂这个问题,先得看看如何定位哈希桶数组索引位置的。
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
我们常用的哈希算法就是除留取余法。但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此JDK团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道HashMap底层数组的长度总是2的n次方,并且取模运算为“h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是HashMap在速度上的优化,因为&比%具有更高的效率。
在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table的length较小的时候,让高位也参与运算,并且不会有太大的开销。
④put方法,最重要的方法,新增
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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为空,或者长度等于0,就resize();相当于初始化;扩容也resize();
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果table上的结点为空的话,直接new Node(),将该结点放在数组下标为i的位子上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果不为空的话,要么是链表,要么是红黑二叉树
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//hash值相同,且key也相同,说明是同一个key ①代码一
e = p;
else if (p instanceof TreeNode)
//树结点 向红黑二叉树添加结点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {//无限循环
if ((e = p.next) == null) {
//添加到next结点
p.next = newNode(hash, key, value, null);
//大于8转换为红黑二叉树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//检验重复key 同①代码一
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果key相同,是否覆盖?
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//留的口子,给后面LinkedHashMap使用
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//用于迭代器并发修改异常
if (++size > threshold)//如果size大于capacity*loadFactory 扩容
resize();
afterNodeInsertion(evict);
return null;
}
hashmap扩容方法
1.什么时候会扩容?初始化的时候、++size > threshold、链表转红黑树的时候,容量小于64 (三种情况)
/**
* resize方法 一种是初始化的时候 二是扩容的时候
*/
final Node<K,V>[] resize() {
//确定newThr和newCap两个的值 start
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//oldTab不为空,扩容 newCap = oldCap << 1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)// capacity 翻倍
newThr = oldThr << 1; // 翻倍
}else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 初始化容量大小 16 threshold 11
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {// 如果newThr没有被赋值,就计算出他的值 <容量*装载因子>
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//确定newThr和newCap两个的值 end
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建新数组
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {//循环old table
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//只有一个结点 直接重新计算位子 放入newTab
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//树节点 遍历树结点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 链表结构:保持顺序不变,1.7是头插法 1.8是尾插法
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//这个地方很巧妙,位运算 等于 在newTab下标不不变 等于1 在newTab下标j+oldTab
//原因是tab的capacity大小是 2的幂次方
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//红黑二叉树的结点重新分配
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到调用此方法的节点
TreeNode<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历
next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点
e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
++lc; // 统计原索引位置的节点个数
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
++hc; // 统计索引位置为原索引+oldCap的节点个数
}
}
if (loHead != null) { // 原索引位置的节点不为空
if (lc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead; // 将原索引位置的节点设置为对应的头结点
// hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (hiHead != null)
loHead.treeify(tab); // 以loHead为根结点, 构建新的红黑树
}
}
if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空
if (hc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
// loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (loHead != null)
hiHead.treeify(tab); // 以hiHead为根结点, 构建新的红黑树
}
}
}
2.查询 get(K key)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//真正获取结点
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) {
if (first.hash == hash && // always check first node
// 不为空,hash值相同 且对象地址相同 或者 hash值相同且equal相同
((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;
}
3.删除 public V remove(Object key)
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) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {//第一个结点不匹配 查找next
if (p instanceof TreeNode)//树的查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
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);
}
}
//是否比较值 key一致 值不一致 是否删除 matchValue
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)//树形结点删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);//预留口子 给LinkedHashMap使用
return node;
}
}
return null;
}
总结
- HashMap的底层是个Node数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
- 增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到key的hashCode值;2)将hashCode的高位参与运算,重新计算hash值;3)将计算出来的hash值与(table.length - 1)进行&运算。
- HashMap的默认初始容量(capacity)是16,capacity必须为2的幂次方;默认负载因子(load factor)是0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
- HashMap在触发扩容后,阈值会变为原来的2倍,并且会进行重hash,重hash后索引位置index的节点的新分布位置最多只有两个:原索引位置或原索引+oldCap位置。例如capacity为16,索引位置5的节点扩容后,只可能分布在新报索引位置5和索引位置21(5+16)。
- 导致HashMap扩容后,同一个索引位置的节点重hash最多分布在两个位置的根本原因是:1)table的长度始终为2的n次方;2)索引位置的计算方法为“(table.length - 1) & hash”。HashMap扩容是一个比较耗时的操作,定义HashMap时尽量给个接近的初始容量值。
- HashMap有threshold属性和loadFactor属性,但是没有capacity属性。初始化时,如果传了初始化容量值,该值是存在threshold变量,并且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值作为新表的capacity值,然后用capacity和loadFactor计算新表的真正threshold值。
- 当同一个索引位置的节点在增加后达到9个时,并且此时数组的长度大于等于64,则会触发链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持。链表节点转红黑树节点的具体方法为源码中的treeifyBin(Node<K,V>[] tab, int hash)方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
- 当同一个索引位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的untreeify(HashMap<K,V> map)方法。
- HashMap在JDK1.8之后不再有死循环的问题,JDK1.8之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
- HashMap是非线程安全的,在并发场景下使用ConcurrentHashMap来代替。
参考:
-
JDK1.8源码
-
HashMap详解 https://blog.csdn.net/v123411739/article/details/78996181