HashMap底层源码解析以及相应问题的思考
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
//默认初始化容量(必须是的2幂) - 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空内容的数组
static final Entry<?,?>[] EMPTY_TABLE = {};
//hash数组/hash表 - new Entry[16];
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//元素个数
transient int size;//0
//阈值
int threshold;//12
//负载因子
final float loadFactor;//0.75f
//外部操作数
transient int modCount;//0
//hash种子数
transient int hashSeed = 0;//xxxx
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//initialCapacity - 16
//loadFactor - 0.75
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))//NaN - Not a Number
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//子类的初始化挂钩 -- 让LinkedHashMap重写
void init() {
}
//key - new Student("王麻子", '男', 22, "2402", "003"),
//value - "写代码"
public V put(K key, V value) {
//第一次添加元素时进入的判断
if (table == EMPTY_TABLE) {
inflateTable(threshold);//初始化工作
}
if (key == null)
return putForNullKey(value);
//获取key的hash值 --> hash - 20
int hash = hash(key);
//利用hash值计算在数组中的下标 --> i - 4
int i = indexFor(hash, table.length);
//注意:进入此循环就说明hash碰撞了
//e - 0x004
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))) {
//oldValue - 看抖音
V oldValue = e.value;
e.value = value;//覆盖原有的value值
e.recordAccess(this);
return oldValue;//返回被替换的值
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
//h - 20
//length - 16
static int indexFor(int h, int length) {
//0001,0100
//0000,1111
return h & (length-1);
}
//k - new Student("李四", '男', 21, "2402", "002")
final int hash(Object k) {
int h = hashSeed;
//如果k是String类型,计算hash值就有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);
}
//value - 抚琴
private V putForNullKey(V value) {
//注意:进入此循环就说明hash碰撞了
//e - 0x001
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;//oldValue - 赏花
e.value = value;//重新替换value
e.recordAccess(this);
return oldValue;//返回被替换的值
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
//hash - 20
//key - new Student("王麻子", '男', 22, "2402", "003"),
//value - "看抖音"
//bucketIndex - 4
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断何时扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
//重新计算hash值
hash = (null != key) ? hash(key) : 0;
//重新计算下标
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//newCapacity - 32
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;//oldCapacity - 16
//老的长度等于map的最大值,就将int的最大值赋值给阈值,意味减少不必要的扩容逻辑
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];//new Entry[32];
//transfer() -> 1.重新计算hash种子数 2.将原有数组中的数据复制到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//将新数组的地址赋值给源数组
table = newTable;
//重新计算阈值 - threshold=24
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;
}
}
}
//hash - 20
//key - new Student("王麻子", '男', 22, "2402", "003"),
//value - "看抖音"
//bucketIndex - 4
void createEntry(int hash, K key, V value, int bucketIndex) {
//JDK1.7插入数据为头插法
//e - 0x003
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//toSize - 16
private void inflateTable(int toSize) {
//capacity - 16
int capacity = roundUpToPowerOf2(toSize);
//threshold - 12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//table = new Entry[16];
table = new Entry[capacity];
//根据容量生成hash种子数
initHashSeedAsNeeded(capacity);
}
//number - 16
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//映射关系类/节点类
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;// ------ 键
V value;// ---------- 值
Entry<K,V> next;// -- 下一个节点的地址
int hash;// --------- key的hash值
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
}
HashMap<Student, String> map = new HashMap<>();
map.put(null, "赏花");//null
map.put(null, "抚琴");//赏花
map.put(new Student("张三", '男', 23, "2402", "001"), "踢足球");//null
map.put(new Student("李四", '男', 21, "2402", "002"), "看电影");//null
map.put(new Student("王麻子", '男', 22, "2402", "003"), "看抖音");//null
map.put(new Student("王麻子", '男', 22, "2402", "003"), "写代码");//看抖音
JDK1.7版的HashMap数据结构是什么?
一维数组+单向链表
HashMap的默认容量是多少?
16
HashMap阈值的作用是什么?
元素个数达到阈值,就考虑扩容
HashMap的默认负载因子是多少?其作用是什么?
0.75
作用:数组长度*负载因子=阈值,负载因子决定扩容比例
HashMap的默认负载因子为什么是0.75?
0.75取得了空间和时间的平衡
负载因子过大(1),会导致数组装满后才扩容,利用了空间,浪费时间
负载因子过小(0.1),会导致数组装了一点点数据就扩容,利用了时间,浪费空间
HashMap的容量为什么必须是2的幂?
2的幂的数字 -1有个特点:原本1的位置变为0,1后面的位置变为1
计算hash值是 数组长度-1 & key的hash值,2的幂长度会导致下标结果分布更加散列
HashMap最大容量为什么多少?
1<<30
HashMap最大容量为什么是1<<30?
最大容量是int类型,1<<30为int类型取值范围里最大的2的幂的数字
HashMap的hash桶是什么?
单向链表
HashMap中的hash冲突/hash碰撞什么是原因出现的?如何优化?
多个key的hash值一样,意味着下标也是一样,在底层就会将多个key存储在一个单向链表里,效率极低
优化:key所属的类重写hashCode()和equals()
HashMap的hash种子数的作用是什么?
key为String类型时,计算hash值会让hash种子数参与
因为不用的字符串的hash值有可能相同,会造成hash碰撞,所以hash种子数就应运而生了~~
HashMap何时扩容?
元素个数大于等于阈值 并且 添加新元素的下标上不等于null
HashMap的hash回环/hash死循环是什么情况下产生?
多线程的情况
一个线程在遍历
一个线程在不断添加导致扩容,扩容时Entry的地址形成回环
经验:HashMap明确说明该实现不是一个线程安全的,在多线程下考虑使用ConcurrentHashMap
JDK1.7和JDK1.8版本的HashMap有什么区别
JDK1.7版本的HashMap,数据结构是一维数组+单向链表,获取hash值使用位移计算,头插法
JDK1.8版本的HashMap,数据结构是一维数组+单向链表+红黑树,获取hash值使用高16位^低16位,尾插法
JDK1.8版本的HashMap数据结构的变化?
数组长度大于64并且单向链表的长度大于8,一维数组+单向链表 -> 一维数组+红黑树
红黑树的节点小于6,一维数组+红黑树 -> 一维数组+单向链表
JDK1.8版本的HashMap单向链表为什么大于8从单向链表变为红黑树?
因为泊松分布