前言
-
hashMap在数据结构上叫做散列表,其本意就是将元素尽量均匀分布到每个位置。
-
jdk1.7中的hashMap底层实现: 一个数组 + 多个单向链表
-
影戏hashMap性能的主要是两个参数:数组初始化长度 + 加载因子。由这两个参数
的合理设置,在时间和空间上保持最好的平衡。 -
fail-fast机制,即迭代器创建后,如果还去更新hashmap,再去操作迭代器时会抛出异常ConcurrentModificationException
代码中的常量和变量
//默认初始化长度
static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子 size/capacity = loadFactor
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//放元素的数组 要求长度是2的n次幂
transient Entry<K,V>[] table;
//已经放入的元素的个数
transient int size;
// capacity * load factor
int threshold;
//加载因子
final float loadFactor;
//map被改变的次数
transient int modCount;
构造器
构造器参数都是围绕capacity(数组长度)和loadFactor(加载因子)
public HashMap(int initialCapacity, float loadFactor) {
// 找一最靠近initialCapacity的2的次幂数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
// sun.misc.VM.isBooted默认是true
// Holder.ALTERNATIVE_HASHING_THRESHOLD 默认是Integer.MAX_VALUE
// 所以这里useAltHashing 默认是false
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
// 这里+1 因为 第一个构造器里面 capactity < initcapacity 没等号
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
几个重要的方法
hash
这个方法是用来获取加入进来的键值对 key的hash值
final int hash(Object k) {
int h = 0;
// 如果JVM没有特别设置 useAltHashing默认false
// 所以一般不会走进去
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
// 默认为0
h = hashSeed;
}
//这三步获取key的hash值
//为什么要重新计算hashcode值,而不是将它直接去取模
// 因为length太小了,导致hashcode只有低位参与运算
// 而将它右移,让高位参与到运算,减少碰撞概率
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor
用来获取键值对 即将放入数组的哪一个下标
static int indexFor(int h, int length) {
// 将键值对 key的hash值 和 (数组长度 - 1) 取模获取下标
// 本来取模是 % 这里用按位与& 是因为
// 当length = 2 的 n 次幂
// h % length = h & (length - 1)
// & 的效率肯定比 % 要高
return h & (length-1);
}
get
public V get(Object key) {
if (key == null)
// hashmap中将key==null的键值对总是放在table[0]中
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object 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;
// hash值相等 且 (== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
put
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
// 已经有该key了,就覆盖原来的value,并返回原来的value
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))) {
V oldValue = e.value;
e.value = value;
// recordAccess是一个空方法,只要出现覆盖,就调用
e.recordAccess(this);
return oldValue;
}
}
// 修改次数+1
modCount++;
// 如果之前map中没有该key,添加一个键值对
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果map里面已经装入的元素的个数 >= 数组长度 * 加载因子
//并且数组该下标已经有一个键值对的情况下
//对table扩容, 数组新的长度是原来的2倍
// 这种扩容的条件其实就是保证数组有元素的 每个位置放入的元素个数差不多
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //单独说这个方法
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 创建新的键值对
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//hashmap采用的是头插法
//意思就是新加入的元素,会被放在链表最前面,成为新的head
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
resize
单独看这个扩容方法,是因为多线程环境操作该方法,扩容后的数组,可能会有一个循环链表
即 a ⇄ b -> null, 这样的后果是遍历该链表时,会死循环
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 在没有特殊情况下,rehash还是false
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
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;
}
}
}
假如有一条链表是这样的,里面有2个节点对象
Entry<key, value> A => Entry<key, value> B
A.key = A B.key = B
A.value = A B.value = B
A.next = B B.next = null
1.x线程执行到关键位置停止,切换到y线程。x中next = e.next = B。
2.y线程执行完transfer方法停止,切换回x线程,此时节点A,B对象已经发生变化
假如新的table中它们还是在一个下标位置上,那么此时
B.next = A , A.next = null; B => A => null
3.x线程继续执行,第一次循环,此时x线程中的newTable中还没有值
e.next = A.next = newTable[i] = null
newTable[i] = e = A (此时A是head 链表是 A =>null)
e = next = B
4.第二次循环
next = B.next = A (因为y线程,B对象发生变化,如果没被修改应该是null)
e.next = B.next = newTable[i](head) = A
newtable[i] = B (此时B变为head)
e = next = A
正常情况下 e = null, 这里就结束了
5.第三次循环
next = A.next = null
e.next = A.next = newTable[i](head) = B
newTable[i] = A
e = next = null
结束 此时链表变成 A ⇄ B