前言
和分析JDK1.8的HashMap源码一样,本文也是通过构造方法、增删改查等基本操作来分析JDK1.7的源码。
源码分析
用到的实例常量
1 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始容量:2^4 = 16 |
用到的实例变量
1 | int threshold; //扩容阀值 |
Entry(链表节点)
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
需要用到的方法
inflateTable
初始化哈希桶,保证哈希桶的容量是2的n次幂。1
2
3
4
5
6
7
8
9 private void inflateTable(int toSize) {
//获取初始容量:大于等于toSize的最小的2^n
int capacity = roundUpToPowerOf2(toSize);
//指定新的扩容阀值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化哈希桶
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
resize
传入新桶容量,进行扩容操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果原哈希桶的长度等于最大容量,直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根据新的容量创建新的哈希桶
Entry[] newTable = new Entry[newCapacity];
//将旧桶中的所有元素放入新桶
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//更新哈希桶
table = newTable;
//更新扩容阀值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer
将旧桶中的所有元素转移到新桶中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//将旧桶中的所有元素转移到新桶中
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) { //如果需要重新计算哈希值
e.hash = null == e.key ? 0 : hash(e.key);
}
//i为节点e在新桶中的位置
int i = indexFor(e.hash, newCapacity);
//节点e成为第i个桶的头结点
e.next = newTable[i];
newTable[i] = e;
//e赋值为下一节点
e = next;
}
}
}
hash
根据传入的key计算其hash值1
2
3
4
5
6
7
8
9
10
11 final int hash(Object k) {
int h = hashSeed;
//如果hashSeed不等于0且key为String类型,使用另一个优化后的hash函数
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
可以看到该方法进行了多次移位和异或操作,这样做是为了降低哈希冲突,使得键值对的分布相对均匀
indexFor
该方法传入一个hash值以及哈希桶容量,返回对应哈希桶索引1
2
3
4
5 static int indexFor(int h, int length) {
//由于保证了哈希桶容量length为2的n次幂,所以h与(length-1)进行与操作,
//就相当于对length取模
return h & (length-1);
}
构造方法
和1.8版本一样,1.7版本也有四个构造方法。
默认构造方法,使用默认的初始容量和加载因子1
2
3public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
可以指定初始容量的构造方法1
2
3public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
前两个方法都是调用该构造方法,该方法可同时指定初始容量和加载因子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 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);
//初始容量不能小于0,也不能超过最大容量;加载因子必须大于0
this.loadFactor = loadFactor;
threshold = initialCapacity; //扩容阀值等于初始化容量
init(); //一个默认的空方法
}
和1.8版本不同的是,扩容阀值等于初始化容量。
还有个构造方法,根据传入的Map构造HashMap1
2
3
4
5
6
7
8
9 public HashMap(Map<? extends K, ? extends V> m) {
//先调用构造方法指定扩容阀值和加载因子
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//确定初始容量,指定扩容阀值并初始化哈希桶
inflateTable(threshold);
//将传入Map的所有元素加入到新的HashMap中
putAllForCreate(m);
}
put(增、改)
1 | public V put(K key, V value) { |
put方法的基本步骤:
- 先判断哈希桶是否为空,为空需先初始化哈希桶。
- 判断传入的key是否为null,为null的话需要另行处理:判断第0号哈希桶是否存在key为null的节点,如果存在则更新value并返回旧的value,否则将该键值对放入第0号哈希桶。
- 根据该key的hash值获取到对应的哈希桶,遍历该哈希桶中的链表,如果找到相同的key,则更新value并返回旧value。否则,创建该新节点并将其放入对应的哈希桶,成为该哈希桶新的头结点,原头结点成为它的下一节点。然后方法结束,返回null。
putForNullKey
1 | private V putForNullKey(V value) { |
addEntry
1 | void addEntry(int hash, K key, V value, int bucketIndex) { |
createEntry
1 | void createEntry(int hash, K key, V value, int bucketIndex) { |
remove(删)
1 | public V remove(Object key) { |
removeEntryForKey
1 | final Entry<K,V> removeEntryForKey(Object key) { |
删除节点的步骤如下:
- 如果当前键值对的数量为0,直接返回null
- 根据传入的key计算出其hash值,并找到对应的哈希桶。遍历该哈希桶中的链表,如果找到要删除的节点就删除并返回该节点,否则返回null。
get(查)
1 | public V get(Object key) { |
getForNullKey
1 | private V getForNullKey() { |
getEntry
1 | final Entry<K,V> getEntry(Object key) { |
get方法的执行步骤:
- 先判断要查找的key是否为null,为null的话另行处理。无论key是否为null,都要先判断当前键值对数量是否为0,为0直接返回null。
- 如果key为null,则遍历第0号哈希桶的链表,如果找到key为null的键值对,返回其value,否则返回null。
- 如果key不为null,先计算该key的hash值,然后根据该hash值找到相应的哈希桶。遍历该哈希桶的链表,如果找到对应的key,返回该键值对的value,否则返回null。
小结
这里小结一下查看了源码后,对1.7版本HashMap的一些新认识:
- 单个键值对存储在Entry类中,利用Entry数组存储多个键值对,利用拉链法来解决哈希冲突,多个冲突的键值对利用链表存储起来。
- 一开始的扩容阀值(在构造方法中确定)等于初始容量(该初始容量不一定是2的n次幂),但是在初始化哈希桶数组时,哈希桶数组的长度是大于等于扩容阀值的最小的2的n次幂,也即是最终哈希桶数组的初始长度是大于等于初始容量的最小的2的n次幂(和1.8版本一样)。
- key为null的键值对放在第0号哈希桶。
- 当键值对数量达到扩容阀值时,进行扩容并重新分配元素的位置,新的容量是原来的两倍。