HashMap是基于哈希表的Map接口的实现,以键值对(key, value)的形式存在。在HashMap中,key-value总是会当作一个整体来处理,系统会根据hash算法来计算key-value的存储位置,我们可以通过key快速的存、取value。下面我们将会对HashMap的源码进行详细的解读,主要围绕:
- HashMap基本源码剖析(构造方法、存、取元素等)
- HashMap内部实现基本点分析
- 容量、负载因子、树化
一、定义
HaspMap实现了Map接口,继承AbstractMap类。其中Map接口定义了键映射到值的规则, AbstractMap类提供Map接口的实现方法。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
二、数据结构
HashMap内部结构是数组(Node[] table)和链表结合组成的复合结构,数组被分成一个个桶(bucket),通过哈希值决定键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储。需注意的是:链表大小超过阈值(TREEIFY_THRESHOLD = 8)时,链表就会被改造成树形结构。 下面示意图为一个简单的HashMap示意图:
三、常量
1. 默认容量
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默认初始化的容量时16,必须是2的幂次方。
2. 最大容量
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
最大的容量是2^30。
3. 默认负载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载因子是0.75
4. 树化阈值:由链表转换成红黑树
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
一个桶中bin的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8
5. 解树化阈值:由红黑树转换成链表
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
当执行resize扩容操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6 。
6. 最小红黑树的容量
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
7. 总结
DEFAULT_INITIAL_CAPACITY = 1 << 4(桶个数16)
DEFAULT_LOAD_FACTOR = 0.75f(负载因子0.75)
TREEIFY_THRESHOLD = 8(树化阈值8)
MIN_TREEIFY_CAPACITY = 64(树化要求的最少哈希表元素数量)
UNTREEIFY_THRESHOLD = 6(解除树化的阈值,在resize阶段)
四、成员变量
在我们学习HashMap时,总会听到负载因子、阈值、容量等变量的名字,那么我们在先对HashMap中的成员变量做一个介绍。
1. table
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
* 这个数组在首次使用时初始化,并根据需要调整大小。分配空间时,长度始终是2的幂次方。
* 我们也在一些操作中允许数组长度为0,目前不需要引导机制
*/
transient Node<K,V>[] table;
总结:
- table是一个用于存放键值对的数组。
- 第一次使用(插入元素)时被初始化,根据需要可以重新分配空间(扩容)。
- 分配的空间长度必须是2的幂次方。
2. entrySet
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
* 保存缓存的entrySet()。注意,它的父接口中用于keySet()和values()方法中有使用
*/
transient Set<Map.Entry<K,V>> entrySet;
当被调用entrySet时被赋值。通过keySet()方法可以得到Map中的key集合,通过values可以得到Map中的value集合。
3. size
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
该值用于存放Map中键值对的个数。
4. modCount
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
HashMap被结构性修改的次数。(结构性修改是指改变了KV映射数量的操作或者修改了HashMap的内部结构。modCount的使用我们在后面的putVal()中有使用到。
5. threshold
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
阈值,当HashMap中的键值对数量超过了阈值,就会扩容。
thresold = capacity * loadFactor
6. loadFactor
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
负载因子
HashMap的成员变量中没有capacity变量。不过我们可以通过threshold和loadFactor计算得到capacity。
五、构造函数
HashMap提供了三个构造函数:
- HashMap():构造一个具有默认初始容量(16)和默认的负载因子(0.75)的空HashMap。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- HashMap(int initialCapacity):构造一个具有指定的初始容量和默认负载因子(0.75)的空HashMap。
public HashMap(int initialCapacity) {
// 在这里调用了两个参数的构造方法,传入指定的初始化容量和默认的负载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- HashMap(int initialCapacity, float loadFactor):构造一个具有指定的初始化容量和负载因子的空HashMap.
public HashMap(int initialCapacity, float loadFactor) {
// 1. 指定的初始化容量小于0,抛出异常IllegalArgumentException
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: "+
initialCapacity);
// 2. 调整最大容量
// 指定的初始化容量大于集合的最大容量MAXIMUM_CAPACITY
// static final int MAXIMUM_CAPACITY = 1 << 30;
if (initialCapacity > MAXIMUM_CAPACITY)
// 将指定的初始化容量改为集合的最大容量MAXIMUM_CAPACITY
initialCapacity = MAXIMUM_CAPACITY;
// 3. 负载因子为非正值,抛出异常IllegalArgumentException
// Float.isNaN(loadFactor):该方法用于描述非法的float数
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 4. 指定的负载因子为正值,将其赋给HashMap的负载因子
this.loadFactor = loadFactor;
// 5. 将传入的初始化容量initialCapacity做计算,返回一个大于等于initialCapacity最小的2的幂次方。
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor(initialCapacity): 将传入的initialCapacity做计算,返回一个大于等于initialCapacity的最小2的幂次方。(就是不管你传入的初始化Hash桶的长度参数为多少,最后通过这个方法会将Hash表的初始化长度改为2的幂次方 )
例如:你确定的initialCapacity=6,计算出来的结果就为8。
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;
}
tableSizeFor()方法详解请看博文:https://blog.csdn.net/meng_lemon/article/details/88874499
在构造函数这里提到了两个参数:初始容量、负载因子。这两个参数是影响HashMap性能的重要参数。
- 容量: 哈希桶中的数量
- 初始容量: 创建哈希表时的容量
- 负载因子: 哈希表在容量增加可以达到的最大的尺度,它是衡量一个散列表的空间使用成都,负载因子越大表示散列表的装填成都越高。
通过构造函数来看,桶数组在最开始的时候并没有初始化好,仅仅设置了一些初始值,这是我们可能会怀疑HashMap是按照lazy-load(懒加载)原则,在首次使用时初始化。我们这时候会想到ArrayList,它是在第一次添加元素使用(添加元素)时才初始化数组,开辟数组空间。
put()方法详解请看博文:
https://blog.csdn.net/meng_lemon/article/details/88906980
在put()方法中,需要初始化数组,开辟数组空间,在数组中元素超过阈值时,常常需要扩容。那么这些都是怎么实现的?我们可以知道一个resize()方法。
resize()方法详解请看博文:https://blog.csdn.net/meng_lemon/article/details/88907241