042:深入Jdk7版本HashMap扩容源码分析
1 HashMap7深度源码分析课程介绍
课程主要内容:
1、HashMap空的Key底层是如何存放
2、HashMap每次扩容原理是什么?
3、HashMap如何实现减少index下标冲突问题
4、HashMap的加载因子为什么要是0.75而不是0.8
2 HashMap7Get方法深度源码分析
@Override
public V get(K key) {
// 计算hash值
int hash = hash(key);
// 根据hash值计算数组下标存放的位置
int index = indexFor(hash, tables.length);
// 遍历链表
for (Entry e = tables[index]; e != null; e = e.next) {
Object k;
// hashCode相同且值相同情况
if (e.hash == hash && ((k = e.getKey()) == key || e.getKey().equals(key))) {
return (V) e.getValue();
}
}
return null;
}
public class Test001 {
public static void main(String[] args) {
MayiktHashMap<Object, String> objectStringMayiktHashMap = new MayiktHashMap<>();
String a = "a";
Integer b = new Integer(97);
objectStringMayiktHashMap.put(a, "蚂蚁课堂A");
objectStringMayiktHashMap.put(b, "蚂蚁课堂B");
System.out.println(objectStringMayiktHashMap.get(a));//蚂蚁课堂A
System.out.println(objectStringMayiktHashMap.get(b));//蚂蚁课堂B
}
}
3 HashMap7添加Key为空源码分析
/**
* key为null时,存放数组0对应链表首位置 put方法判断key为null调用
*
* @param value
* @return
*/
private V putForNullKey(V value) {
for (Entry<K, V> e = tables[0]; e != null; e = e.next) {
if (e.getKey() == null) {
V oldValue = e.getValue();
e.setValue(value);
return oldValue;
}
}
// 否则情况下,存放到数组0对应链表首位置
addEntry(0, null, value, 0);
return null;
}
……
// get方法判断key为null调用
public V getForNullKey() {
// 循环遍历数组0对应的链表(第一个链表)
for (Entry e = tables[0]; e != null; e = e.next) {
if (e.getKey() == null) {
return (V) e.getValue();
}
}
return null;
}
public class Test002 {
public static void main(String[] args) {
MayiktHashMap<Object, String> objectStringMayiktHashMap = new MayiktHashMap<>();
objectStringMayiktHashMap.put(null, "蚂蚁课堂666");
System.out.println(objectStringMayiktHashMap.get(null));//蚂蚁课堂666
}
}
4 HashMap7巧妙运用位于运算
103&15=7, 103&16=0
104&15=8, 104&16=0
105%15=9, 105&16=0
如果长度不减1,可能发生index冲突,导致整个链表过长,查询效率降低
(注意:hashMap频繁发生hash冲突或者index冲突都会导致查询效率降低)
5 HashMap7位与运算二进制原理
h & (length - 1)巧妙运用
& - 参加运算的两个数据,按二进制位进行“与”运算。
运算规则:0&0=0, 0&1=0,1&0=0,1&1=1。(同1得1,否则为0)
h & (length-1); h为hash值,length-1
103&(16-1)即103&15
0110 0111
0000 1111
0000 0111
得到二进制为0000 0111,转换为十进制7
103&16
0110 0111
0001 0000
0000 0000
得到二进制为0000 0000,转换为十进制0
length-1 就是为了防止下标冲突存放到同一个链表中,查询效率会降低。
h & (length-1) 本质是取模运算。
位运算(&)效率要比取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
X % 2^n = X & (2^n–1)
2^n 表示2的n次方,也就是说,数X对2^n 取模==数X和(2^n–1)做按位与运算。
6 HashMap7底层扩容原理
void addEntry(int hash, K key, V value, int bucketIndex) {
// 当size>=阈值 且 存在index冲突的情况下才开始扩容
if ((size >= threshold) && (null != tables[bucketIndex])) {
// 假设当前容量为16,阈值16*0.75=12,已经有12个元素均摊到下标1-13位置 新元素下标index=0,此时位置足够没必要扩容
// 目的是保证每个数组都有存放元素,避免浪费资源
// 新数组长度为原table长度*2
resize(2 * tables.length);
hash = (null != key) ? hash(key) : 0;
// 计算扩容后index
bucketIndex = indexFor(hash, tables.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
// 获取原来的tables
Entry[] oldTables = tables;
// 获取原来tables长度
int oldCapacity = oldTables.length;
// 如果原数组长度为限制最大容量,阈值为Integer最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建新容量数组
Entry[] newTables = new Entry[newCapacity];
// 假设原数组长度16,新数组长度32,原index=hash&15,新index=hash&32,下标不对应
// 每次扩容的时候需要重新计算index值赋值到新tables中
// 重新计算index值
transfer(newTables);
tables = newTables;
// 新的扩容阈值为新容量*0.75 eg:32*0.75=24
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 重新计算index下标
*
* @param newTables
*/
private void transfer(Entry[] newTables) {
// 获取新数组长度
int newCapacity = newTables.length;
// 遍历数组首链表节点
for (Entry<K, V> e : tables) {
// 遍历链表
while (null != e) {
// 获取下一个节点
Entry<K, V> next = e.next;
// 计算节点新位置
int index = indexFor(e.hash, newCapacity);
// 将e插入newTables[index]链表首节点
e.next = newTables[index];
newTables[index] = e;
// 继续下一个节点处理
e = next;
}
}
}
7 HashMap7断点调试扩容方法
public class Test003 {
public static void main(String[] args) {
MayiktHashMap<Object, Object> objectObjectHashMap = new MayiktHashMap<>();
for (int i = 1; i <= 12; i++) {
objectObjectHashMap.put("mayikt_" + i, "mayikt_" + i);
}
objectObjectHashMap.put("A", "每特");
}
}
断点调试:
8 HashMap7为什么加载因子为0.75
为什么加载因子是0.75 而不是0.8 /1 呢?
加载因子越大,index下标冲突概率越大,反而空间利用率更高;
加载因子越小,index下标冲突概率越小,反而空间利用率不是非常高,频繁扩容;
index下标冲突概率越大,链表长度更长,查询的成本非常高。因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。
附录 手写简单版本HashMap1.7实现基本功能源码
public class MayiktHashMap<K, V> implements MayiktMap<K, V> {
// 默认初始容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 加载因子 0.75f 作用:数组扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 最大初始容量 10亿+
static final int MAXIMUM_CAPACITY = 1 << 30;
// 实际加载因子
final float loadFactor;
/**
* 阈值 需要扩容的时候实际hashMap存放的大小
* 容量*加载因子 当达到阈值情况下开始扩容
*/
int threshold;
//hashMap底层数组 初始为空
transient Entry<K, V>[] tables = null;
// 集合大小
transient int size;
public MayiktHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public MayiktHashMap(int initialCapacity, float loadFactor) {
// 初始容量校验
if (initialCapacity < 0) {
throw new IllegalArgumentException("初始容量不符" + initialCapacity);
}
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
}
// 校验加载因子
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("加载因子不符" + loadFactor);
}
// 设置加载因子和实际hashMap存放的阈值
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
/**
* 定义空方法便于子类扩展功能
*/
protected void init() {
}
/**
* 注意:hash冲突或者index冲突,都会存放到同一个链表中
*
* @param key
* @param value
* @return
*/
@Override
public V put(K key, V value) {
//数组为空,初始化数组
if (tables == null) {
inflateTable(threshold);
}
if (key == null) {
return putForNullKey(value);
}
// 计算hash值
int hash = hash(key);
// 根据hash值计算数组下标存放的位置
int index = indexFor(hash, tables.length);
// 判断HashCode相同情况,循环遍历链表
for (Entry e = tables[index]; e != null; e = e.next) {
Object k;
// hashCode相同且值相同情况
if (e.hash == hash && ((k = e.getKey()) == key || e.getKey().equals(key))) {
//获取原值value
Object oldValue = e.getValue();
//设置新value
e.setValue(value);
// 返回原value
return (V) oldValue;
}
}
// 值不同情况,添加元素
addEntry(hash, key, value, index);
return null;
}
@Override
public V get(K key) {
if (key == null) {
return getForNullKey();
}
// 计算hash值
int hash = hash(key);
// 根据hash值计算数组下标存放的位置
int index = indexFor(hash, tables.length);
// 遍历链表
for (Entry e = tables[index]; e != null; e = e.next) {
Object k;
// hashCode相同且值相同情况
if (e.hash == hash && ((k = e.getKey()) == key || e.getKey().equals(key))) {
return (V) e.getValue();
}
}
return null;
}
public V getForNullKey() {
// 循环遍历数组0对应的链表(第一个链表)
for (Entry e = tables[0]; e != null; e = e.next) {
if (e.getKey() == null) {
return (V) e.getValue();
}
}
return null;
}
/**
* key为null时,存放数组0对应链表首位置
*
* @param value
* @return
*/
private V putForNullKey(V value) {
for (Entry<K, V> e = tables[0]; e != null; e = e.next) {
if (e.getKey() == null) {
V oldValue = e.getValue();
e.setValue(value);
return oldValue;
}
}
// 否则情况下,存放到数组0对应链表首位置
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 当size>=阈值 且 存在index冲突的情况下才开始扩容
if ((size >= threshold) && (null != tables[bucketIndex])) {
// 假设当前容量为16,阈值16*0.75=12,已经有12个元素均摊到下标1-13位置 新元素下标index=0,此时位置足够没必要扩容
// 目的是保证每个数组都有存放元素,避免浪费资源
// 新数组长度为原table长度*2
resize(2 * tables.length);
hash = (null != key) ? hash(key) : 0;
// 计算扩容后index
bucketIndex = indexFor(hash, tables.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
// 获取原来的tables
Entry[] oldTables = tables;
// 获取原来tables长度
int oldCapacity = oldTables.length;
// 如果原数组长度为限制最大容量,阈值为Integer最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建新容量数组
Entry[] newTables = new Entry[newCapacity];
// 假设原数组长度16,新数组长度32,原index=hash&15,新index=hash&32,下标不对应
// 每次扩容的时候需要重新计算index值赋值到新tables中
// 重新计算index值
transfer(newTables);
tables = newTables;
// 新的扩容阈值为新容量*0.75 eg:32*0.75=24
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 重新计算index下标
*
* @param newTables
*/
private void transfer(Entry[] newTables) {
// 获取新数组长度
int newCapacity = newTables.length;
// 遍历数组首链表节点
for (Entry<K, V> e : tables) {
// 遍历链表
while (null != e) {
// 获取下一个节点
Entry<K, V> next = e.next;
// 计算节点新位置
int index = indexFor(e.hash, newCapacity);
// 将e插入newTables[index]链表首节点
e.next = newTables[index];
newTables[index] = e;
// 继续下一个节点处理
e = next;
}
}
}
private void createEntry(int hash, K key, V value, int bucketIndex) {
// 获取原来Entry对象,如果获取为空,没有发生hash冲突
Entry<K, V> next = tables[bucketIndex];
// 新建一个Entry对象,放在tables对应数组位链表首个位置,如果原来Entry有值,新Entry指向原Entry
tables[bucketIndex] = new Entry(hash, key, value, next);
}
/**
* 计算hash值
*
* @param k
* @return
*/
final int hash(Object k) {
int h = 0;
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值计算index下标位置
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);
}
/**
* 默认数组初始化
*
* @param toSize
*/
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
// 计算初始化容量
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 数组中的容量初始化
tables = new MayiktHashMap.Entry[capacity];
}
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;
}
@Override
public int size() {
return size;
}
class Entry<K, V> implements MayiktMap.Entry<K, V> {
// key
private K k;
// value
private V v;
// next 指向下一个Entry
private Entry<K, V> next;
// 存放Entry对象的hash值
private int hash;
public Entry(int hash, K key, V value, Entry<K, V> next) {
this.hash = hash;
this.k = key;
this.v = value;
this.next = next;
size++;
}
@Override
public K getKey() {
return this.k;
}
@Override
public V getValue() {
return this.v;
}
@Override
public V setValue(V value) {
this.v = value;
return this.v;
}
public Entry(K k, V v) {
this.k = k;
this.v = v;
}
}
}