这篇博客里整理了HashMap源码中比较重要,需要掌握和探究的点,也是一些在面试中常常遇到的问题~
包括以下问题——
1、HashMap的key、value都可以为null,映射不是有序的,并且不保证该序列恒久不变。
2、HashMap是线程不安全的——
HashMap不是同步的,通过Collections类的静态方法synchronizedMap——
public static
返回由指定映射支持的同步(线程安全的)映射。
3、HashMap数据结构
HashMap底层是基于数组和链表来实现的。
查询速度快的原因:通过计算散列码来决定存储位置。
计算hash值:通过key的HashCode来计算。(只要HashCode相同,hash值就相同)
hash冲突:不同对象所算出的hash值可能是相同的。
HashMap底层如何解决hash冲突:链表。
图解:
4、初始化容量为什么为16?
首先,假设HashMap的长度是10
hashcode : 101110001110101110 1001
length - 1 : 1001
index : 1001
再换一个hashcode 101110001110101110 1111 试试:
hashcode : 101110001110101110 1111
length - 1 : 1001
index : 1001
从结果可以看出,虽然hashcode变化了,但是运算的结果都是一样,为1001,也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111),这样就不符合hash均匀分布的原则;
反观长度16或者其他2的幂,length - 1的值是所有二进制位全为1,这种情况下,index的结果等同于hashcode后几位的值,只要输入的hashcode本身分布均匀,hash算法的结果就是均匀的
所以,HashMap的默认长度为16,是为了降低hash碰撞的几率
5、加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
为什么是0.75?
若:加载因子越大,填满的元素越多,好处是空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高
因此——必须在"冲突的机会"与]空间利用率"之间寻找一种平衡。
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。
一般我们都不用去设置它,用默认值0.75即可。
为什么加载因子的默认值为0.75?
泊松分布——在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布。
用0.75作为加载因子的值的时候,桶内元素的个数和概率对照表:
上面的对照表显示:当加载因子为0.75时,桶中的元素个数达到8个的概率已经很小了;
即表明——0.75作为hashmap的加载因子的时候,每个碰撞位置的链表长度几乎都在8个
以下了。
6、HashMap 中关于红黑树的三个关键参数
HashMap 中有三个关于红黑树的重要参数:
1、static final int TREEIFY_THRESHOLD = 8;
一个桶的树化阈值:当桶中元素个数超过8的时候,需要使用红黑树节点替换链表节点
2、static final int UNTREEIFY_THRESHOLD = 6;一个树的链表还原阈值:当扩容时,桶中元素个数小于等于6的时候,就会把树形的桶元素 还原(切分)为链表结构3、static final int MIN_TREEIFY_CAPACITY = 64;哈希表的最小树形化容量:当哈希表中的容量大于这个值时,表中的桶才能进行树形化
注:桶内元素太多,且容量小于64时,会扩容,而不是树形化
7、为什么哈希表的容量一定要是2的整数次幂?
首先,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;
其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。
(如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间)
所以,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
8、HashMap的扩容(resize)机制:
为什么要扩容(重新计算容量):在向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时———对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
代码解析:
//传入新的容量
void resize(int newCapacity) {
//引用扩容前的Entry数组 Entry[] oldTable = table; int oldCapacity = oldTable.length;
//扩容前的数组大小如果已经达到最大(2^30)了 if (oldCapacity == MAXIMUM_CAPACITY) {
//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了 threshold = Integer.MAX_VALUE; return; }
//初始化一个新的Entry数组 Entry[] newTable = new Entry[newCapacity];
//将数据转移到新的Entry数组里 transfer(newTable);
//HashMap的table属性引用新的Entry数组 table = newTable;
//修改阈值 threshold = (int)(newCapacity * loadFactor);//修改阈值 }
transfer()方法:将原有Entry数组的元素拷贝到新的Entry数组里。
注意:要重新计算每个元素在数组中的位置(hash值)。
遍历顺序:先遍历到哈希表的第一个元素,然后依次遍历以这个元
素为头结点的单链表中的每个元素。接着遍历哈希表中第二个元素……
重复该过程。
9、HashMap存取时,要计算当前key应该对应Entry[]数组哪个元素,即计算
数组下标,代码如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
效率更高的原因:位运算直接对内存数据进行操作,不需要转化为十进制,处理速度更快。
10、HashMap和HashTable中对于hash的实现的总结——
HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1倍。
原因:
HashTable选择取模运算:当哈希表的大小为素数时,简单的取模哈希的结果会更加
均匀,hash结果越分散效果越好。
HashMap选择取模运算:在取模计算时,如果模数是2的幂,那么我们可以直接使用
位运算来得到结果,运算效率要大大高于做取模。
但是,HashMap为了提高效率使用位运算代替哈希——引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。
11、扰动运算
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。
简单来说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
12、HashMap共有4个构造函数:
①HashMap()
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
②HashMap(int initialCapacity)
构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
③HashMap(int initialCapacity, float loadFactor)
构造一个带指定初始容量和加载因子的空HashMap。
④HashMap(Map
构造一个映射关系与指定 Map 相同的新 HashMap。
9、put方法 (将“key-value”添加到HashMap中)
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到哈希表的下标0的位置中。
if (key == null)
return putForNullKey(value);
//若“key不为null”,计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到哈希表中
modCount++;
addEntry(hash, key, value, i);
return null;
}
10、get方法 (获取key对应的value)
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
11、fail-fast
fail-fast:java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
实现及原理:这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。