前言:
HashMap是我们在编程过程中非常常见的一个数据结构
主要功能是存放键值对(K-V)
我们都知道HashMap在jdk7中是以数组+链表的形式构成的(JDK8的改动还是比较大的,数组+链表/红黑树构成)
HashMap中数组和链表的关系如下
类似这个形状:
那么HashMap源码的原理到底是怎么样的呢,我们来分析一下
一、图解流程
二、源码分析
分析一:put方法分析
1.我们new一个HashMap对象
2.点进方法
这里有两个成员变量,通过看注释我们知道
DEFAULT_INITIAL_CAPACITY是默认初始容量-必须是2的幂。
DEFAULT_LOAD_FACTOR是构造函数中未指定时使用的负载因子。
3.我们使用put存入一个键值对
点进put方法查看
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//若table空 就是哈希表没有初始化
//使用构造函数时设置的阈值(初始容量)初始化数组table
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);//如果key是null,就没办法计算hash值,所以放到table的第一个位置
int hash = hash(key);//根据key计算hash值的方法
int i = indexFor(hash, table.length);//根据hash值 获得key应该存放的数组table中位置
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);
//返回被覆盖掉的value
return oldValue;
}
}
modCount++;
//该key不存在,放入链表中(jdk7是采用的头插的方法)
addEntry(hash, key, value, i);
return null;
}
put方法流程图如下:
4.看初始化数组方法
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);//这个方法是将toSize值转变成2的幂,如果toSize=6,则capacity=8,2的4次方
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//重新计算阈值 threshold = 容量 * 加载因子
table = new Entry[capacity];//用新计算的容量初始化数组table
initHashSeedAsNeeded(capacity);
}
分析二:hash计算分析
final int hash(Object k) {
//将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.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);
}
但是这个hash值并不是数组的下标还要有一个运算才能得出下标:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);//将hash值与数组长度-1作与运算就是数组的下标
}
以上的种种干扰变化可以有效的减少碰撞
分析三:transfer扩容机制分析:
我们在put方法的时候发现一个给数组赋值的方法addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//发现数组长度不够了
resize(2 * table.length);//调用扩容函数,新数组的长度要求是2倍的旧数组长度
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
扩容函数:
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);//新的阈值
}
转移数组函数:
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;//链表下一个节点,防止链表丢失
}
}
}
流程图解:
分析四:获取元素源码分析
get方法
public V get(Object key) {
if (key == null)//如果key是null直接返回数组第一个元素,因为数组第一个元素就是放的key=null
return getForNullKey();
Entry<K,V> entry = getEntry(key);//获取value方法
return null == entry ? null : entry.getValue();
}
进入getEntry方法查看
final Entry<K,V> getEntry(Object key) {
if (size == 0) {//数组长度为0,没有元素也就是获取不到就返回null
return null;
}
int hash = (key == null) ? 0 : hash(key);//计算该key的hash值
for (Entry<K,V> e = table[indexFor(hash, table.length)];//indexFor函数算出下标,根据下标获取键值对对象
e != null;
e = e.next) {//遍历该位置的链表直到找到对应的key,如果没有旧直接返回null就好了
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
有了put方法的基础,get方法不论实现还是理解都是比较好理解的
获取元素流程图解:
到这里我们分析完了HashMap的put和get最主要的两个方法,这篇博客先写这么多,休息一下太累了