HashMap简介
HashMap是Java语言中的一种集合类,它实现了Map接口,用于存储Key-Value对。它基于哈希表数据结构,通过计算Key的哈希值来快速定位Value的位置,从而实现高效的插入、删除和查找操作。下面我们对照着JAVA1.8中的HashMap源码来分析一下它的内部实现逻辑
基本的结构
在开始分析HashMap的实现逻辑之前,我们需要先了解一下基础的组成和内部的成员变量都有哪些,分别代表什么意思。
1、Node<K,V>
首先我们看一下HashMap其中一个子类:Node<K,V>,这个子类用于存储基本的元素,即Key-Value对、Key的Hash值以及指向下一个节点的Node<K,V>变量。在HashMap内部,由Node<K,V>类型组成的数组用来存储所有的元素。 Node<K,V>实现自Map.Entry<K,V>接口,并且实现了接口中规定的多个基本方法:
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
...
}
同时,在Node<K,V>类中,定义了4个成员变量:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable,Serializable {
....
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;
}
...
}
...
}
其中hash是key的hash值,key和value存储键和值,next变量指向链表中的下一个元素。
2、HashMap的成员变量
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
table:保存所有元素的数组。entrySet:一个用于遍历所有数据节点的集合。size:记录HashMap中元素的总数量。modCount:用来判断在对HashMap数据项进行遍历时,其中的数据项是否有修改过,如删除或者新增一项。 threshold:控制扩容时机,当数据项数量大于threshold时进行扩容,新的容量大小是老的两倍。loadFactor:默认值0.75,加载因子决定threshold大小,计算公式是threshold=table.length*loadFactor。
我们先大致了解一下HashMap成员变量及基础的Key-Value承载的结构,之后随着介绍的进度我们再介绍新的类型。下面我们开始正式分析HashMap的逻辑。
初始化方法
HashMap有4个初始化方法,分别是:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// MAXIMUM_CAPACITY = 1 << 30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
第一个初始化方法有两个参数:initialCapacity和loadFactor,看参数名initialCapacity好像是控制初始化时HashMap容量大小的,实际上它不直接控制大小,而是通过tableSizeFor方法计算出threshold的值,此时threshold为大于等于传入的initialCapacity的2的次幂最小值。比如传入3,那么threshold=222^222=4,如果传入9,则threshold=242^424=16。loadFactor初始化HashMap的成员变量loadFactor。
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
而实际控制容量大小的逻辑在添加第一个元素时确定,现在先放一边不管,等到介绍添加逻辑时再分析。
第二个构造函数很简单,直接调用了第一个构造函数,传入initialCapacity和默认的加载因子DEFAULT_LOAD_FACTOR,默认加载因子是0.75。
第三个是无参的构造函数,没有设置threshold,只设置了默认的加载因子0.75。
第四个构造函数则是使用一个现有的Map对象进行初始化操作,首先设置好默认的加载因子,然后利用putMapEntries方法初始化数据项。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
//若传入的Map为空,则不进行初始化操作
if (s > 0) {
//初始化时,HashMap中还没有任何元素,所以table为null,此时根据传入的map大小计算出threshold。
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//非初始化(例如调用putAll方法)时,如果传入的map大小大于threshold,则进行resize扩容操作。
else if (s > threshold)
resize();
//遍历传入的map,依次调用putVal方法将所有数据加到当前HashMap对象中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
这个方法中所调用的resize和putVal方法在其他地方也有调用,我们在put方法的实现中再详细分析,此处只需要知道这个构造函数是通过其他Map对象构造HashMap对象的。
现在已经了解了它的基本结构和所有的构造函数,我们用一张图先直观的看一下HashMap是什么样的。

在这个HashMap对象中,变量table长度等于8,size等于3,threshold等于6。当元素个数大于6时,table将被扩容到16个,threshold也会变为12。
操作
1、put操作
put操作的实现逻辑是调用一个内部不可重写的方法putVal实现,这个方法有5个入参,分别是Key的Hash值、Key、Value、onlyIfAbsent、evict。onlyIfAbsent表示是否覆盖相同Key的Value值,为true时,只有原来的Value值为null时才会覆盖,否则不覆盖。为false时直接覆盖原值。下来我们直接看源码并逐行分析。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, 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赋值给局部变量tab并判断是否为null,如果为null,或者不为null则将长度赋给局部变量n,并判断长度是否0。
* 条件成立的话调用resize()方法对table进行初始化,并将初始化后的table长度重新赋值
本文详细剖析了Java中HashMap的数据结构、成员变量、初始化过程、插入、删除、查找操作的逻辑,包括Node子类、成员变量如table、size、threshold等的作用。文中还探讨了put操作的实现,包括哈希冲突的解决、链表与红黑树的转换,并介绍了删除和查找元素的方法。此外,还提及了HashMap的其他相关方法和集合操作。
最低0.47元/天 解锁文章

707

被折叠的 条评论
为什么被折叠?



