1 HashMap的默认构造方法
HashMap底层采用数组+链表数据结构进行存储。每次新增一个数据,就会被插入到数组中的空位。慢慢的数据多了,就会存在哈希碰撞问题,即2个key的索引一样。HashMap采用的解决方法是链地址法。数组中每个元素设置链表。key映射到数组某个元素中,而数据项本身插入到元素的链表中。
//数组的默认容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 默认负载因子为0.75,建议不要设置超过 0.75,因为会显著增加冲突,降低 HashMap的性能。;过低也不行,会导致频繁扩容,导致HashMap性能降低。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//生成一个Entry<K,V>类型的空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//无参构造函数
public HashMap() {
//调用有参构造函数
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//有参构造函数
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))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//负载因为默认为0.75
this.loadFactor = loadFactor;
//初始化时,扩容阀值为threshold为16
threshold = initialCapacity;
//空方法
init();
}
2 新增key-value数据
2.1 put()方法
新增key-value值调用put()方法,里面是调用到很多其他方法,下面我们一一展开
//判断是否为空的数组
if (table == EMPTY_TABLE) {
//首次扩容数组
inflateTable(threshold);
}
// 若“key为null”,则将该键值对添加到数组的第0个元素中
if (key == null)
return putForNullKey(value);
//求key的哈希值
int hash = hash(key);
//获取待插入元素在数组中的下表
int i = indexFor(hash, table.length);
//待插入位置table[i]不为空,则查找是否有相同key,若有则替换alue
,并将老的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;
//空方法
e.recordAccess(this);
return oldValue;
}
}
//修改次数加1
modCount++;
//“待插入位置table[i]为空” 或者 “待插入位置table[i]不为空但链表没有找到相同key的节点
//新的key-value插入到数组中
addEntry(hash, key, value, i);
//返回空值
return null;
}
2.2 数组首次扩容
private void inflateTable(int toSize) {
//获取一个数值=2^n >= toSize
int capacity = roundUpToPowerOf2(toSize);
//扩容阀值一般是容量 * 负载因子,首次添加数据时,阀值为16 * 0.75 = 12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//生成一个容量为16的Entry数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
public static int highestOneBit(int i) {
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
2.2.1 highestOneBit()方法
highestOneBit(int i)是为了获取不大于i的max(2 ^n)
。其中>>n
表示向右移动n位,相当于除以2^n
。其中,
>>>n
表示向右无符号移动n位,左边移动的空位都补0,相当于除以2^n
,i |= (i >> n)
等价于i = i | (i >> n)
, ‘|’
是按位或操作:如果两个操作数对应的位都是0才为0,否则为1。
int i = 1073741824;
System.out.println(i + " || " + Integer.toBinaryString(i));
i |= (i >> 1);
System.out.println(i + " || " +l Integer.toBinaryString(i));
i |= (i >> 2);
System.out.println(i + " || " + Integer.toBinaryString(i));
i |= (i >> 4);
System.out.println(i + " || " + Integer.toBinaryString(i));
i |= (i >> 8);
System.out.println(i + " || " + Integer.toBinaryString(i));
i |= (i >> 16);
System.out.println(i + " || " + Integer.toBinaryString(i));
运算结果:
1073741824 || 1000000000000000000000000000000
1610612736 || 1100000000000000000000000000000
2013265920 || 1111000000000000000000000000000
2139095040 || 1111111100000000000000000000000
2147450880 || 1111 1111 1111 1111 000000000000000
2147483647 || 1111111111111111111111111111111
执行这5次按位或运算是为了找到和i的二进制最高位一样,且其他位数位1的二进制数。
2.2.2 roundUpToPowerOf2()方法
roundUpToPowerOf2(int number) 获取不小于number的最小2^n
值
如果去掉(number - 1) << 1
中-1
,则获得的值2^n > number
,加上1
则有可能获得2^n = number
,即最小的不小于number的2^n
值。
int i = 8;
System.out.println(Integer.highestOneBit(i << 1));
System.out.println(Integer.highestOneBit((i - 1) << 1));
运行结果:
16
8
2.2 空key
HahsMap的key是可以为空的,key为null则放在数组的第一个元素中,而HashTable是不允许的,否则会抛出NullPointerException。
private V putForNullKey(V value) {
//判断链表中是否存在空key,若有则替换value,并将balue返回
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
2.3 重新hashcode()方法
重写 hashcode()方法是为了得更散列的哈希值。用了很多次的右移动运算,是为了让高位也参与预算。另外两个数之间的运算采用^
。这是因为|
和&
操作的结果分别趋于1
和0
,会造成数据不均问题。
final int hash(Object k) {
//哈希种子,默认为0
int h = hashSeed;
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);
}
2.4 插入数组的索引
static int indexFor(int h, int length) {
return h & (length-1);
}
我们第一想法是通过求余操作,但是HashMap的实现是通过&
操作的方法,这种方法的效率会比较高。
length - 1
代表索引的范围是[0, length-1]
.这里要引伸出一个点,即数组的容量capacity要设计成2^n
。如果是非2^n
,如下面的9
,capacity2 - 1
后为1000
,则&
运算后,只能得到1000
或0000
这两个索引值,会造成数据分布不均。
int capacity = 8;
System.out.println(Integer.toBinaryString(capacity));
System.out.println(Integer.toBinaryString(capacity - 1));
运行结果:
1000
111
int capacity2 = 9;
System.out.println(Integer.toBinaryString(capacity2));
System.out.println(Integer.toBinaryString(capacity2 - 1));
运行结果:
1001
1000
2.5 将key-value添加到Entry中
put插入数据,将KV放到Entry中,如果待插入数组的bucketIndex位置不为空,则采用头插法,新插入的数据的指针指向数组中旧的数据,形成链表,同时将待插入数据放到数组bucketIndex位置中。
key为null则放在数组的第一个元素中,而value为null则封装到节点Entry的value中。
正是这两点,使得HashMap的key-vaule都可以为空。
void addEntry(int hash, K key, V value, int bucketIndex) {
// HahsMap中已添加元素的总数(size)超过扩容阀值且待插入索引不为空,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容为旧的数组长度的2倍
resize(2 * table.length);
//判断key是否为空
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) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//Entry是单链表中的一个节点
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//指向链表的下一个节点
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
...
}
2.6 数组再次扩容
扩容是调用resize方法,扩容后的数组为旧容量的2倍。resize方法首先判断旧的容量大小是否等于MAXIMUM_CAPACITY(2^30
) ,如果是则直接赋予int类型得最大值2^31 -1
。
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;
}
}
}
2.7 哈希种子
空数组首次扩容中提到如何修改哈希种子。hashSeed默认为0,只有switching为tue才会发生改变。
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
跟进入Holder.ALTERNATIVE_HASHING_THRESHOLD
,可以发现ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
的默认值是2^31 - 1
,除非用户修改
系统变量jdk.map.althashing.threshold
的值,否则数组的长度基本上是会比ALTERNATIVE_HASHING_THRESHOLD_DEFAUL
小,哈希种子是不会改变的。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//HashMap的静态私有内部类
private static class Holder {
static{
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
3 查询key-value数据
查询使用get()方法,掌握上面put()方法后,get()还是比较好理解。
public V get(Object key) {
//key为空时,使用数组中第一个元素
if (key == null)
return getForNullKey();
//key不为空时,找到索引位置,从链表头开始比对value,符合则返回,不符合就返回null
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//空key
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
//非空key
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
4 删除
删除使用rermove()方法。
public V remove(Object key) {
//根据key找到索引位置,从链表头开始比对value,符合则返回,不符合就返回null
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
//判断容量是否为空,为空则直接返回空
if (size == 0) {
return null;
}
//找到key对应的索引
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
//指向待删除节点的前驱节点
Entry<K,V> prev = table[i];
//指向待删除节点
Entry<K,V> e = prev;
//遍历列表
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
//更改次数加1
modCount++;
//总数量减1
size--;
//如果待删除节点位于链表头,则prev == e
if (prev == e)
table[i] = next;
else
//待删除节点的前驱节点的指针指向待删除节点的下一个节点,完成删除过程
prev.next = next;
e.recordRemoval(this);
return e;
}
//节点prev和e移动分别移动一位
prev = e;
e = next;
}
return e;
}
5 修改
修改和新增都是使用put()方法,如果旧的key存在,则value值会被覆盖掉,返回旧的value。
6 实现克隆接口
先看一段测试代码
HashMap<String, String> myMap = new HashMap();
myMap.put("1", "a");
for (Map.Entry<String, String> entry : myMap.entrySet()) {
System.out.println("myHashMap: {key:" + (String)entry.getKey() + ", value:" + entry.getValue() + "}");
}
HashMap<String, String> cloneMap = (HashMap<String, String>)myMap.clone();
for (Map.Entry<String, String> entry : cloneMap.entrySet()) {
System.out.println("cloneMap: {key:" + (String)entry.getKey() + ", value:" + entry.getValue() + "}");
}
System.out.println("");
myMap.put("1", "aa");
for (Map.Entry<String, String> entry : myMap.entrySet()) {
System.out.println("myHashMap: {key:" + (String)entry.getKey() + ", value:" + entry.getValue() + "}");
}
for (Map.Entry<String, String> entry : cloneMap.entrySet()) {
System.out.println("cloneMap: {key:" + (String)entry.getKey() + ", value:" + entry.getValue() + "}");
}
运行结果:
myHashMap: {key:1, value:a}
cloneMap: {key:1, value:a}
myHashMap: {key:1, value:aa}
cloneMap: {key:1, value:a}
从测试结果可以发现,当myHashMap中的value改变时,克隆后的cloneMap的value不变。从源码也可以看出,首先根据旧的HashMap对象克隆一个新的实例result,
紧接着调用putAllForCreate(),将旧的HashMap对象中的key-value赋值给result。
public Object clone() {
HashMap<K,V> result = null;
try {
//克隆当前对象
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
if (result.table != EMPTY_TABLE) {
result.inflateTable(Math.min(
(int) Math.min(
size * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY),
table.length));
}
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
// 将全部元素添加到新生成的HashMap中
result.putAllForCreate(this);
return result;
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
//复制旧的HashMap实例中的key-value值到克隆后的实例中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
7 实现序列化接口
先看一段测试代码
HashMap<String, String> myMap = new HashMap();
myMap.put("1", "a");
FileOutputStream fos = new FileOutputStream("a.txt");
ObjectOutputStream oo = new ObjectOutputStream(fos);
//序列化.写入文件中
oo.writeObject(myMap);
FileInputStream fis = new FileInputStream("a.txt");
ObjectInputStream oi = new ObjectInputStream(fis);
//反序列化
HashMap<String, String> myMap2 = (HashMap<String, String> )oi.readObject();
for (Map.Entry<String, String> entry : myMap2.entrySet()) {
System.out.println("反序列化: {key:" + (String)entry.getKey() + ", value:" + entry.getValue() + "}");
}
//关闭流
oi.close();
fis.close();
oo.close();
fos.close();
运行结果:
反序列化: {key:1, value:a}
看完这段代码,我们会猜想:HashMap获取索引的地址是通过key做哈希算法
public native int hashCode()
,是native方法,即不同平台计算的值可能不一样,
那么反序列化后通过key去查value不就乱套?跟进去源码
//序列化
private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
if (table==EMPTY_TABLE) {
s.writeInt(roundUpToPowerOf2(threshold));
} else {
s.writeInt(table.length);
}
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (size > 0) {
//将key-value依次写入序列化中,不是以数组形式写入
for(Map.Entry<K,V> e : entrySet0()) {
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}
private static final long serialVersionUID = 362498820763181265L;
//反序列化
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// set other fields that need values
table = (Entry<K,V>[]) EMPTY_TABLE;
// Read in number of buckets
s.readInt(); // ignored.
// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
// capacity chosen by number of mappings and desired load (if >= 0.25)
int capacity = (int) Math.min(
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
// allocate the bucket array;
if (mappings > 0) {
inflateTable(capacity);
} else {
threshold = capacity;
}
init(); // Give subclass a chance to do its thing.
//依次读取key-value值,并写入新的数组,保证了key-value数据的一致性
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
HashMap为了避免不同平台key的哈希值不一致导致key-value数据错误,自己重写了序列化和反序列化,核心点是序列化时将key-value依次写入序列化中,反序列化时依次读取key-value值,写入新的数组,解决key-value数据错误问题。
8 死锁问题
HashMap被设计为单线程环境下的高性能容器,但是在多线程插入数据时候会发生死锁,导致一个CPU飙到100%。并发条件下建议替换成ConcurrentHashmap。陈皓老师的博文https://coolshell.cn/articles/9606.html写的非常简单易懂,这里不再累述。
9 ConcurrentModificationException异常
9.1 异常产生原因
HashMap一边遍历,一边执行删除操作,马上就抛出ConcurrentModificationException
异常。
public class HashMapApp {
public static void main(String[] args) {
HashMap<String, String> myMap = new HashMap<>();
myMap.put("1", "a");
myMap.put("2", "b");
for (Map.Entry<String, String> entry : myMap.entrySet()) {
if(entry.getKey().equals("2"))
myMap.remove(entry.getKey());
System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
}
}
}
key:2, value:b
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:922)
at java.util.HashMap$EntryIterator.next(HashMap.java:962)
at java.util.HashMap$EntryIterator.next(HashMap.java:960)
at collection.HashMapApp.main(HashMapApp.java:20)
直接看源码,抛出异常的原因是修改次数modCount和期望修改次数expectedModCount不一致造成的。
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
查看经Javac编译后的代码HashMapApp.class文件,可以发现遍历的本质还是使用迭代器Iterator。
public class HashMapApp {
public HashMapApp() {
}
public static void main(String[] args) {
HashMap<String, String> myMap = new HashMap();
myMap.put("1", "a");
myMap.put("2", "b");
Entry entry;
for(Iterator i$ = myMap.entrySet().iterator(); i$.hasNext(); System.out.println("key:" + (String)entry.getKey() + ", value:" + (String)entry.getValue())) {
entry = (Entry)i$.next();
if (((String)entry.getKey()).equals("2")) {
myMap.remove(entry.getKey());
}
}
}
}
因此代码可以调整为:
public static void main(String[] args) {
HashMap<String, String> myMap = new HashMap();
myMap.put("1", "a");
myMap.put("2", "b");
Map.Entry entry;
Iterator i$ = myMap.entrySet().iterator();
while(i$.hasNext()) {
entry = (Map.Entry)i$.next();
if (((String)entry.getKey()).equals("2")) {
myMap.remove(entry.getKey());
}
System.out.println("key:" + (String)entry.getKey() + ", value:" + (String)entry.getValue());
}
}
在抛出异常的代码处打断点
在IDEA工具debug模式下,右下方有方法调用的过程,进去next()的源码,可以发现EntryIterator是继承了HashIterator。
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
跟进去HashIterator,在构造函数中可以发现预期的修改次数expectedModCount是与modCount相等。从上面的put()和remove()源码可知,每次执行put()和remove()方法都会执行modCount++
。这就造成了expectedModCount != modCount
,nextEntry方法抛出ConcurrentModificationException()异常!!!
private abstract class HashIterator<E> implements Iterator<E> {
'''
int expectedModCount; // For fast-fail
HashIterator() {
expectedModCount = modCount;
...
}
...
}
9.2 fast-fail机制
问题就来了,为什么要HashMap要判断modCount != expectedModCount
?
这还是因为HashMap不适合并发条件的原因。一个线程在遍历,一个线程在删除,就会有数据不同步原因。HashMap采用快速失败(fast-fail)的机制,尽早将抛出异常,终止这种行为!
10 参考文献
1)疫苗:JAVA HASHMAP的死循环 https://coolshell.cn/articles/9606.html
2) JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
3) Bruce Eckel,Java编程思想 第4版. 2007, 机械工业出版社