HashMap是Java中常用的数据结构之一,它是基于哈希表
实现的,用于存储
和管理
键值对。HashMap内部是由一个Entry数组
和一个链表
组成的,数组用于存储
数据,链表用于解决哈希冲突
。HashMap会根据实际需要,动态地调整数组的长度,以保证高效的查找、插入和删除操作。
其主要特点包括:
总之,HashMap是一种高效的数据存储和查找结构,它在Java中得到广泛应用,特别是在缓存、索引、状态跟踪等领域。
源码分析(JDK1.8)
/**
* 默认初始容量(必须是2的幂次方)
* 指的是HashMap内部数组的大小
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量,如果任何一个带参数的构造函数隐式指定了更高的值,则使用该容量
* 必须是2的幂次方,最大不能超过1073741824
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 构造函数中未指定时使用的负载系数
* 指的是当HashMap内部数组的元素个数达到数组大小与负载因子的乘积时,会自动扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 红黑树转成链表的长度阀值
* 当链表的长度小于6时,此时的红黑树会重新转换成链表结构
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 链表转化成红黑数的长度阈值
* 当数组的长度大于等于64,且链表的长度大于8时,链表转红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 将链表转话为红黑树的数组长度阀值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 哈希桶,在
*/
transient Node<K,V>[] table;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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; //负载因子 0.75
//创建一个2的幂次容量大小的数组(对指定的容量值,取最大的2次幂)
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR; //负载因子 0.75
putMapEntries(m, false);
}
HashMap中的put方法用于将一个键值对(key-value)映射添加到HashMap中。其中,key表示要存储的键,value表示要存储的值。该方法的返回值为新添加的值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
具体实现过程如下:
HashMap的remove方法用于从数组中删除键值对,并返回其对应的值;删除方法有两个,删除指定key和删除指定key和指定value,二者的区别在于,
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) {
//如果数组table不为null,且hashCode对应的index槽位有节点
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有指向,说明当前槽位存在hash冲突,结构可能是链表或者红黑树
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);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//通过key匹配上了节点,如果需要匹配value,则继续校验匹配节点中的value是否与指定value一致
if (node instanceof TreeNode)
//红黑树删除节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
//链表结构,如果待删除节点是头结点,则将其下一个节点作为新的头节点
tab[index] = node.next;
else
//将待删除节点的前一个节点的next指向待删除节点的下一个节点
p.next = node.next;
//更新modCount和size的值
++modCount;
--size;
//调用afterNodeRemoval方法处理删除节点后的操作
//此方法会校验,如果删除后的红黑树节点小于等于6时,会重新将红黑树转成链表
afterNodeRemoval(node);
return node; //返回删除节点
}
}
//数组为null,或者key未匹配上待删节点,直接返回null
return null;
}
default boolean remove(Object key, Object value) {
//通过key匹配节点
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
//匹配上的节点中的curValue与指定value不匹配或者key未匹配上节点
return false;
}
//通过key匹配上了待删节点,执行删除操作
remove(key); //此处源码看上一个删除介绍
return true;
}
HashMap中的get方法用于从映射中检索与指定键相关联的值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 通过key值获取对应节点信息
*/
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
((k = first.key) == key || (key != null && key.equals(k))))
//通过哈希值和key匹配上了头节点,直接返回头节点
return first;
if ((e = first.next) != null) {
//与头节点不匹配,但是头节点的next不为空,说明头节点的尾部有其他节点,此数组槽位是一个链表结构
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))))
//链表,通过key依次遍历链表节点
return e;
} while ((e = e.next) != null);
}
}
//如果槽位头节点为空,说明该槽位上不存在任何节点,直接返回null
return null;
}
具体实现过程如下:
因为它可以保证在默认负载因子下(0.75)具有较好的性能
和空间利用率
。
默认情况下,HashMap 的负载因子为 0.75,这意味着在 HashMap 中存储的键值对数量如果超过容量的 0.75 倍时,就会自动进行扩容,而每次扩容都会将原先的键值对重新分配到新的桶中,这样就会浪费很多时间。因此,如果初始容量设置过小,会导致频繁的扩容,浪费时间和空间;如果初始容量设置过大,又会浪费大量的空间。而 16 恰好是一个 2 的整数次幂,所以 HashMap 内部使用位运算的方式进行哈希计算,速度比取模运算快,实现简单。因此,在默认负载因子下,初始容量为 16 是一个较好的选择。
/**
* 返回一个2的指数次幂的容量值
* 该值必须比指定容量cap大,且最接近cap的2的指数次幂
* 例如:指定容量cap为11,那么创建的HashMap初始容量就是16
*/
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;
}
因为HashMap 在进行扩容时,会将容量扩大到原来的两倍,这样可以更快地进行一些位运算,提高运行的效率
。为了实现这个扩容操作,HashMap 初始容量必须是 2 的整数次幂,否则在扩容时位运算方法会失效
,导致性能降低
。
因此,为了确保 HashMap 的高效性能,在指定初始容量的时候,强制将其改为 2 的整数次幂。
在 Java 中的 HashMap 是基于哈希表实现的,哈希表的核心是数组,而数组的大小是固定的。当我们往 HashMap 中添加元素时,如果当前位置已经有元素了,则会发生碰撞,这个时候 HashMap 会依据一定的规则将元素添加到数组的其他位置。
如果想要提高 HashMap 的性能,就需要尽可能减少碰撞。一种有效的方式就是通过调整数组的大小,增加数组的容量,从而使得元素分布更加均匀,减少碰撞的发生。
而为了在扩容后保持哈希表中原有元素的位置不变,HashMap 采用了重新哈希的方法来处理。在重新哈希时,需要将原有元素重新计算哈希值,并放在新的哈希表中的相应位置上。如果新哈希表的大小不是原哈希表大小的两倍,则计算哈希值时需要进行更复杂的位运算,效率更低。因此,为了在扩容时能够尽可能提高性能
,HashMap 选择将哈希表的大小扩大为原来的两倍。
在哈希表中,每个位置都可能有多个键值对,它们是通过链表或红黑树来存储的。在节点查找时,哈希表首先需要根据键的哈希值和表长算出键值对应的位置,称之为“槽位”(slot)。然后哈希表会在该槽位上查找对应的节点。
在哈希表中,每个链表(或红黑树)的第一个节点是该链表的头节点,也就是存储在槽位中的第一个节点。因此,如果某个键对应的节点存在于哈希表中,那么它一定存储在对应槽位的链表(或红黑树)中。
因此,在哈希表的getNode方法中,首先判断头节点是否等于目标节点,如果是,则直接返回头节点。这样可以快速判断目标节点是否位于对应槽位的链表(或红黑树)中,如果不是,则遍历链表(或红黑树)的所有节点寻找目标节点,直到找到或者遍历完整个链表(或红黑树)。这样可以提高查找效率
。