JDK7-HashMap源码分析
Posted 程序员小潘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK7-HashMap源码分析相关的知识,希望对你有一定的参考价值。
为了方便阅读和写注释,笔者将 HashMap 源码单独拷出来了,推荐大家也这么做,阅读起来更加轻松,Debug 也很方便,更重要的是,你可以修改源码来测试。
继承关系图
阅读源码建议采用自上而下的结构,建议先看 Map 接口、然后 AbstractMap 抽象类、最后 HashMap。
整体结构分层
Map 接口
定义 Map 具备的功能,使用内部接口 Entry 来对单个映射关系进行封装,Entry 是 Map 的基本组成单元。
AbstractMap
实现了 Map 接口的抽象类,实现了绝大多数方法,put 和 entrySet 没有实现,因为这两个方法涉及到底层的实现逻辑和数据结构,必须交给子类去实现。
HashMap
HashMap 解析
笔者几乎给每个属性和方法都写上了注释,想要源码的朋友可以私信我。
各属性的含义
/**
* 默认容量:1<<4 = 10000 = 16
* 必须是2的幂次方数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量:1<<30 = 1000000000000000000000000000000 = 1073741824
* 必须是2的幂次方数
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的负载因子,可通过构造器指定
* 扩容时机:当前元素数量 >= 数组容量 * 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 空数组,在HashMap没有put元素时共享
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* 存放Entry的数组,默认是空的
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* 当前包含的 key-value 映射关系的数量
*/
transient int size;
/**
* 扩容的阈值,计算公式:容量 * 负载因子
* 如果table是空的,表示第一次扩容,则根据初始大小来扩容
*/
int threshold;
/**
* 扩容的负载因子
* 如果不指定,则为0.75
*/
final float loadFactor;
/**
* 记录HashMap在结构上修改的次数,和线程并发有关。
*/
transient int modCount;
/**
* 容量的默认阈值,可以通过JVM参数来设置。
* 超过该值,String类型的key会使用另外一种哈希算法,来减少哈希冲突。
* 该值会影响哈希种子修改的时机。
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
/**
* 哈希种子,和计算Key的哈希算法有关
* 当hashSeed!=0 且 Key的类型为String时,则采用Hashing.stringHash32()算法来计算哈希。
* 目的:减少String类型作为Key所带来的哈希冲突。
* 哈希种子何时会改变?去看 initHashSeedAsNeeded()
*/
transient int hashSeed = 0;
内部类 Holder
该类没有任何方法,只有一个属性ALTERNATIVE_HASHING_THRESHOLD
,它只做了一件事:读取 JVM 参数,来设置修改哈希种子的时机。
/**
* 该类只做了一件事:读取JVM参数,来设置修改哈希种子的时机。
*/
private static class Holder {
/**
* 容量超过该值时,采用其他哈希算法,默认Integer.MAX_VALUE,可通过JVM参数设置
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
内部类 Entry 实现了 Map.Entry 接口,将映射关系封装在 Entry 中。因为基于 Hash,所以还记录了 hash 值。因为基于链表结构,所以记录了指向下一个节点的 next。
除此之外,还预留了两个钩子函数给子类扩展。
/**
* 实现了Map.Entry接口,将映射关系封装在Entry中。
* 因为基于Hash,所以还记录了hash值。
* 因为基于链表结构,所以记录了指向下一个节点的next。
*/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* Entry添加时的钩子,也是为了给子类用的,例如:LinkedHashMap
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* Entry删除时的钩子,也是为了给子类用的,例如:LinkedHashMap
*/
void recordRemoval(HashMap<K,V> m) {
}
}
构造器
/**
* 指定初始大小、负载因子来构建Map
* 初始化时,虽然扩容阈值 = 初始大小,
* 但是第一次put时,扩容阈值会被重新设置为:初始大小 * 负载因子
*/
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;
threshold = initialCapacity;
init();
}
/**
* 指定初始大小来构建Map
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 初始大小16 负载因子0.75 来构建Map
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 根据Map来构建一个新的HashMap。
* 初始大小根据旧Map的大小计算而来,负载因子0.75
*/
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);
putAllForCreate(m);
}
构造器的最后,会调用一个 init(),这个方法在 HashMap 中是空的,它其实是提供给子类用的钩子函数。
/**
* 提供给子类初始化用的钩子函数,例如:LinkedHashMap。
*/
void init() {
}
数据结构
HashMap 采用数组+链表的结构来存放数据,画了一下结构图,大致如下图所示:
当我们调用 put()时,HashMap 做了什么?
整理了一下,put()方法内部的大致逻辑,如下图所示:
PUT
/**
* put的逻辑
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//第一次put时,table是空的,需要先扩容(懒加载)
inflateTable(threshold);
}
if (key == null)
//key为null的put逻辑
return putForNullKey(value);
//计算key的哈希
int hash = hash(key);
//计算放在table的哪个下标(放在哪个桶里)
int i = indexFor(hash, table.length);
//遍历链表,判断Key是否已存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//Key已存在,则覆盖
//判断是否覆盖的逻辑可以记一下
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//Key不存在,新增操作
modCount++;
addEntry(hash, key, value, i);
return null;
}
第一次 put 时,table 是空的,需要先扩容(懒加载)
/**
* 容器初始化扩容
* 容器的大小必须为2的幂次方数
* roundUpToPowerOf2()目的:找到 >= toSize的2的幂次方数
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
容器的大小必须是 2 的幂次方,通过 roundUpToPowerOf2()获得
/**
* 找到 >= number 的2的幂次方数
* 因为容器的大小必须为2的幂次方数
* 又是位运算,这个算法很巧妙,可以多研究研究...
*/
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
初始化时判断是否要修改哈希种子,涉及到后面的哈希算法
/**
* 哈希种子是否要修改 -> 是否采用其他哈希算法
* 触发时机:容量 >= Integer.MAX_VALUE (可通过JVM参数设置)
*/
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
初始化完成后,如果 Key 为 null,单独处理
/**
* Key为null时的put逻辑
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//table[0]有元素占用 直接覆盖
if (e.key == null) {
V oldValue = e.value;
e.value = value;
//给子类用的钩子函数
e.recordAccess(this);
return oldValue;
}
}
//table[0]无元素,添加Entry。
//hash为0,且放在table的第0个下标。
modCount++;
addEntry(0, null, value, 0);
return null;
}
哈希算法
/**
* 计算Key的哈希函数
* 如果哈希种子不为0,且Key类型是String,则采用Hashing.stringHash32()哈希算法。
* 哈希种子修改时机,去看 initHashSeedAsNeeded()。
* 默认的哈希算法:拿到Key的hashCode,做了一次再散列。
* 防止开发者重写的hashCode()分散性太差,而导致HashMap的哈希冲突。
* Key为Null,总是会映射到数组的第0个下标。
*/
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
计算放在 table 的哪个下标(放在哪个桶里)
/**
* 根据Key计算的哈希值来判断元素应该放到数组的哪个下标位置
*/
static int indexFor(int h, int length) {
/**
* Java采用的是位运算,并没有采用 取模运算。
* 效果是一样的,但是位运算效率更高。
*/
return h & (length-1);
}
如果 Key 存在则覆盖,不存在,则准备插入,实际插入前,会判断是否需要扩容。
/**
* 将Entry添加到table[bucketIndex]之前的处理逻辑
* 添加之前,先判断是否需要扩容。
* 扩容逻辑:元素 >= 扩容阈值,且 对应桶里的元素不为空。
* 因为:如果桶里元素为空,说明table并没有填充的均匀,只是链表太长,链表过长进行扩容没有意义。
* 理想:元素的分散性好,桶内元素均匀,链表短。
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
//扩容后需要重新计算哈希,因为哈希种子可能被修改
hash = (null != key) ? hash(key) : 0;
//重新计算需要被放到table的哪个下标位置
bucketIndex = indexFor(hash, table.length);
}
//构建Entry,并添加到table[bucketIndex]中。
createEntry(hash, key, value, bucketIndex);
}
扩容逻辑
/**
* Map扩容,逻辑:当前容量 * 2
* 创建新的数组,元素转移
*/
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);
}
扩容后,需要进行元素转移,每次扩容都要判断是否需要重新哈希。
/**
* 转移所有的Entry到新数组中
* rehash:是否需要重新哈希,详情去看initHashSeedAsNeeded(),绝大多数为false
*/
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);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
元素转移后,table 指向新数组,并重新设置扩容阈值。
扩容以后,对将要新插入的元素重新计算哈希,因为哈希种子可能被修改。还要重新计算桶下标,因为扩容后 table.length 已发生变化。
这些工作处理完成以后,才是真正的插入,新元素会插入到桶内链表的头节点,因为效率最高。
/**
* 构建Entry,并添加到table[bucketIndex]中。
* 如果桶内已经有元素,会直接添加到头节点中,因为效率最高,next指向旧的头节点。
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
至此,PUT 操作全部结束。
PUT 完成后,HashMap 会调用 recordAccess 钩子函数,为了子类扩展。
HashMap 的问题
-
为什么官方文档说 HashMap 不保证元素的顺序恒定不变?因为扩容后,元素转移时,链表的顺序会调换。 -
为什么 HashMap 线程不安全?
GET
相较于 PUT,GET 的代码就简单很多了。
/**
* 从容器中根据key获取映射的value
* 返回Null有两种情况:
* 1、Key不存在
* 2、Key映射的value就是Null
* 可以通过containsKey()判断。
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
获取 Key 为 Null 的 value 单独处理,因为 Key 为 null 的元素强制放在 table[0]。
/**
* 获取Key为null对应的value
*/
private V getForNullKey() {
if (size == 0) {
//容器为空,直接返回null
return null;
}
//Key为null的元素强制放在table[0]里
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
Key 不为 Null 就要去数组中寻找了,步骤一样的,先计算哈希,再计算桶下标,再遍历链表查找。
/**
* 获取指定Key的映射关系
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//先计算Key的哈希
int hash = (key == null) ? 0 : hash(key);
//判断在哪个桶里,然后遍历链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
REMOVE
删除的代码也比较简单。
/**
* 从容器中删除指定Key的映射关系,并返回
*/
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
先计算哈希,计算桶下标,遍历链表找到元素,修改一下链表的指向就可以了。
/**
* 根据Key删除Entry
* 先计算哈希,判断在哪个桶里,
* 然后遍历链表,找到了就删除(重新设置链表指向)
*/
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
//要删除的是链表头节点,直接头节点指向next即可。
table[i] = next;
else
//否则,上一个节点的next,指向当前删除节点的next
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
REMOVE 完成后,HashMap 会调用 recordRemoval 钩子函数,为了子类扩展。
其他一些方法
putAll、批量插入。
/**
* 将指定容器中的所有元素添加到该容器中,Key存在会被替换。
* 与putAllForCreate不同的是,该方法会进行扩容
*/
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
if (table == EMPTY_TABLE) {
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
putAllForCreate、不扩容的前提下批量插入,适用于已知长度的情况下提升性能。
/**
* 不扩容的前提下,将旧Map里的元素,添加到当前容器。
* 已知长度的情况下,一次性构建即可,没必要扩容,降低性能。
*/
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
putForCreate、不扩容的插入,反序列化时有用。
/**
* 逻辑和put类似,只是不会去扩容表。
* 作用:反序列化时有用,一次性构建,而不是多次扩容。
*/
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
clone
/**
* 重写clone方法,返回一个元素相同的新容器
* @return
*/
public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
if (result.table != EMPTY_TABLE) {
result.inflateTable(Math.min(
(int) Math.min(
size * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY),
table.length));
}
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
result.putAllForCreate(this);
return result;
}
内部迭代器抽象类:HashIterator,主要为了方便迭代所有的 Key、Value、Entry。
子类有:
-
ValueIterator -
KeyIterator -
EntryIterator
序列化和反序列化的处理
/**
* 序列化时将内容写入到流中。
*/
private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
if (table==EMPTY_TABLE) {
s.writeInt(roundUpToPowerOf2(threshold));
} else {
s.writeInt(table.length);
}
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (size > 0) {
for(Map.Entry<K,V> e : entrySet0()) {
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}
private static final long serialVersionUID = 362498820763181265L;
/**
* 反序列化时从流中构建容器
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// set other fields that need values
table = (Entry<K,V>[]) EMPTY_TABLE;
// Read in number of buckets
s.readInt(); // ignored.
// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
// capacity chosen by number of mappings and desired load (if >= 0.25)
int capacity = (int) Math.min(
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
// allocate the bucket array;
if (mappings > 0) {
inflateTable(capacity);
} else {
threshold = capacity;
}
init(); // Give subclass a chance to do its thing.
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
清空容器
/**
* 清空容器,数组、链表仍然存在,只是将所有桶里的第0个元素置为空。
*/
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
可能有部分代码没有贴出来,如果有遗漏,可以私信笔者。
以上是关于JDK7-HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段
mysql jdbc源码分析片段 和 Tomcat's JDBC Pool
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段