本版本基于android-19 sdk源码分析
上面连篇文章已经分析了一下Arraylist和linkedList源码,那怎么能错过HashMap源码分析呢!
hashMap嘚吧嘚
HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发。
HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
HashMap 底层实现是基于数组+链表实现的。他之所以查询速度快是因为它是通过计算散列码来决定存储位置的。
原理图:
变量分析
private static final int MINIMUM_CAPACITY = 4;
private static final int MAXIMUM_CAPACITY = 1 << 30;
private transient int threshold;
HashMap容量的初始值为4,如果没有指定大小的话!最大值为整型的最大值。
threshold
是阈值,当它的大小超过这个阈值时,表就被重新哈希,一般是0.75.
HashMapEntry(K key, V value, int hash, HashMapEntry next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
首先HashMap里面实现一个静态内部类HashMapEntry,其重要的属性有 key , value, hash,next,从属性key,value我们就能很明显的看出来HashMapEntry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是HashMapEntry[],Map里面的内容都保存在HashMapEntry[]里面。
构造方法
- 无参构造
private static final Entry[] EMPTY_TABLE= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
public HashMap() {
table = (HashMapEntry[]) EMPTY_TABLE;
threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}
无参构造方法创建了一个大小为2的HashMapEntry数组对象。
- 创建指定大小容量hashMap
public HashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
}
//如果传入值为0,则按照无参构造创建tab
if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry[] tab = (HashMapEntry[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
}
//如果传入值小于4,让其等于4,如果传入值大于int最大值,让其等于最大值
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}
private HashMapEntry[] makeTable(int newCapacity) {
//创建指定大小HashMapEntry
HashMapEntry[] newTable = (HashMapEntry[]) new HashMapEntry[newCapacity];
table = newTable;
//阈值大小为总大小的0.75
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}
- 创建指定容量大小和阈值的构造方法
public HashMap(int capacity, float loadFactor) {
this(capacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Load factor: " + loadFactor);
}
/*
* Note that this implementation ignores loadFactor; it always uses
* a load factor of 3/4. This simplifies the code and generally
* improves performance.
*/
}
put方法
public V put(K key, V value) {
//如果key值为空
if (key == null) {
//给entryForNullKey 设置值
return putValueForNullKey(value);
}
//计算key的hash值,
int hash = secondaryHash(key);
HashMapEntry[] tab = table;
//通过hash计算本key在数组中的下标,方便快速查找
int index = hash & (tab.length - 1);
for (HashMapEntry e = tab[index]; e != null; e = e.next) {
//判断链表中是否已经存在此key,如果存在,只需要替换value即可
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
//判断是否需要扩容
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
//把对象增加如链表的头部
addNewEntry(key, value, hash, index);
return null;
}
private V putValueForNullKey(V value) {
//获取当前entryForNullKey
HashMapEntry entry = entryForNullKey;
//如果当前entry为空,则需要新加否则只替换value
if (entry == null) {
addNewEntryForNullKey(value);
size++;
modCount++;
return null;
} else {
preModify(entry);
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry(key, value, hash, table[index]);
}
原理图如下
因此得知。当key的secondaryHash 值相同时,都会被放到同一条链表中。并且链表中刚刚加入的元素都会被放在最前面,加入时间很早的元素会被放到头部。
链表扩容
private HashMapEntry[] doubleCapacity() {
HashMapEntry[] oldTable = table;
//获取老表的长度
int oldCapacity = oldTable.length;
//如果扩容到最大了,不需要扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
//扩容为原来的两倍,为什么只扩容为2倍?
int newCapacity = oldCapacity * 2;
//创建新表
HashMapEntry[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
//开始遍历老表中数据
for (int j = 0; j < oldCapacity; j++) {
HashMapEntry e = oldTable[j];
//如果index个数据为空,不需要copy
if (e == null) {
continue;
}
//根据当前链表的hash和老数组大小计算随机值
int highBit = e.hash & oldCapacity;
HashMapEntry broken = null;
//把需要拷贝的链表在重新的数组中重新定义下标
newTable[j | highBit] = e;
for (HashMapEntry n = e.next; n != null; e = n, n = n.next) {
//计算当前元素和老数组总长度的随机值
int nextHighBit = n.hash & oldCapacity;
//当不想等时
if (nextHighBit != highBit) {
//在新数组指定位置重新创建新链
if (broken == null)
newTable[j | nextHighBit] = n;
else
//否则在后面加入链表即可
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
因此可以发现,每次扩容都是老数组的2倍,并且把老数组中的数据重新copy到了新的数组中,但并不是原封不动的copy,会重新计算index的位置。
get方法
public V get(Object key) {
//当获取key为空时,直接去entryForNullKey获取值
if (key == null) {
HashMapEntry e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = key.hashCode();
hash ^= (hash >>> 20) ^ (hash >>> 12);
hash ^= (hash >>> 7) ^ (hash >>> 4);
HashMapEntry[] tab = table;
//根据key的二次hash获取到下标位置,遍历链表
for (HashMapEntry e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
//如果俩表中有此key,就返回此元素的value
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
get方法比较简单,直接通过key的二次hash获取到链表位置,再遍历链表获取到value。
思考
- hashmap的哈希冲突解决方法:拉链法等。拉链法的优缺点。
- hashmap的参数及影响性能的关键参数:加载因子和初始容量。
- Resize操作的过程。
- hashmap容量为2次幂的原因。
- https://jingyan.baidu.com/article/4ae03de3d52ac23eff9e6bb0.html
- https://blog.csdn.net/qq_36523667/article/details/79657400