看过源码后总结自己对于HashMap1.8源码的一些知识点:
HashMap1.8的底层是数组+链表+红黑树,实际上1.8中转红黑树的概率是非常非常低的,后面会提到。
我们先来分析下底层的主数组,下面是我画的数组,一般要给定数组的长度,那么给定多少合适?一般我们创建数组的时候如果给定int类型,那么数组里每个都是int类型,给定double类型等其它类型也是如此,那么当前数组对应的类型是什么?里面每个对象存放的是啥?带着这些疑问我们往下看。
下面我们来看一串代码
public class Test {
//这是main方法的程序入口
public static void main(String[] args) {
//定义HashMap集合
HashMap<String,Integer> map = new HashMap<>();//数组+链表+红黑树
//集合中添加元素:
System.out.println(map.put("通话", 10));
System.out.println(map.put("随便", 20));
System.out.println(map.put("通话", 30));
System.out.println(map.put("重地", 40));
System.out.println(map.size());
System.out.println(map);
System.out.println("通话".hashCode());
System.out.println("重地".hashCode());
}
}
上面我存的第一个数据是(“通话”,10),这个数据是如何存进去的?大致思路是这样:首先把(“通话”,10)封装成一个对象,其次调用它的哈希值(有一点hashmap基础的应该是能理解的)。接着把(“随便”, 20)放进去,把(“通话”, 30)放进去,发现已经有了(“通话”,10),只能把10替换成30,接着把(“重地”, 40)放进去,这里我分别输出了"通话"对象和"重地"对象对应的哈希码,发现是一样的。
这两个对象的哈希码一样,说明发生了哈希碰撞,实际上刚才(“通话”, 30)把(“通话”, 10)给撞了一下,现在把"重地"和"通话"再撞一下,此时"重地"对象会以链表的形式往外追加。有一句话想必都听过”前7后8“,7与8相差是很大的,当然8更难。
在很多面试中,许多人总是会回答当链表>8的时候会转成红黑树,其实是不对的,实际上还要满足另外一个条件 ——> 主数组长度>=64,这样链表才能转成红黑树。
经过上面一系列补充,可以开始阅读源码了,这里标好了序号,按顺序阅读即可(红黑树源码未贴出)
public class HashMap<K,V>
extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口
//【2】又实现了这个接口,多余,但是设计者觉得没有必要删除,就这么地了
implements Map<K,V>, Cloneable, Serializable{ //【1.5】泛型这里说一下
//【3】后续会用到的重要属性:先粘贴过来:
static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
//定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
//太大容易引起哈西冲突,太小容易浪费 0.75是经过大量运算后得到的最好值
//这个值其实可以自己改,但是不建议改,因为这个0.75是大量运算得到的
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
transient int size;
int threshold;//数组扩容的界限值,门槛值 16*0.75=12
final float loadFactor;//用来接收装填因子的变量
//【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
public HashMap(int initialCapacity, float loadFactor) {
//【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
this.loadFactor = loadFactor;
//【8】数组扩容的界限值,门槛值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//【9】给table数组赋值,初始化数组长度为16
table = new Entry[capacity];
}
//【10】调用put方法:
public V put(K key, V value) {
//【11】对空值的判断
if (key == null)
return putForNullKey(value);
//【12】调用hash方法,获取哈希码
int hash = hash(key);
//【14】得到key对应在数组中的位置
int i = indexFor(hash, table.length);
//【16】如果你放入的元素,在主数组那个位置上没有值,e==null 那么下面这个循环不走
//当在同一个位置上放入元素的时候
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//哈希值一样 并且 equals相比一样
//(k = e.key) == key 如果是一个对象就不用比较equals了
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//【17】走addEntry添加这个节点的方法:
addEntry(hash, key, value, i);
return null;
}
//【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
//k.hashCode()函数调用的是key键值类型自带的哈希函数,
//由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
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 ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//【15】返回int类型数组的坐标
static int indexFor(int h, int length) {
//其实这个算法就是取模运算:h%length,取模效率不如位运算
return h & (length-1);
}
//【18】调用addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//【25】size的大小 大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
if ((size >= threshold) && (null != table[bucketIndex])) {
//【26】主数组扩容为2倍
resize(2 * table.length);
//【30】重新调整当前元素的hash码
hash = (null != key) ? hash(key) : 0;
//【31】重新计算元素位置
bucketIndex = indexFor(hash, table.length);
}
//【19】将hash,key,value,bucketIndex位置 封装为一个Entry对象:
createEntry(hash, key, value, bucketIndex);
}
//【20】
void createEntry(int hash, K key, V value, int bucketIndex) {
//【21】获取bucketIndex位置上的元素给e
Entry<K,V> e = table[bucketIndex];
//【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
//【23】将新的Entry放在table[bucketIndex]的位置上
table[bucketIndex] = new Entry<>(hash, key, value, e);
//【24】集合中加入一个元素 size+1
size++;
}
//【27】
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//【28】创建长度为newCapacity的数组
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//【28.5】转让方法:将老数组中的东西都重新放入新数组中
transfer(newTable, rehash);
//【29】老数组替换为新数组
table = newTable;
//【29.5】重新计算
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//【28.6】
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);
}
//【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
int i = indexFor(e.hash, newCapacity);
//【28.8】头插法
e.next = newTable[i];//获取链表上元素给e.next
newTable[i] = e;//然后将e放在i位置
e = next;//e再指向下一个节点继续遍历
}
}
}
}
补充(两道经典面试题)
1.装填因子(负载因子或加载因子)为什么是0.75?
假若装填因子设置为1:空间利用率得到了很大的满足,很容易发生碰撞,产生链表–>查询效率低;
假若装填因子设置为0.5:碰撞的概率低,扩容1,产生链表的几率低,查询效率高,空间利用率太低;
因此取0.5-1的中间值:0.75,官方给的源码注释中也表明了:
// /* <p>As a general rule, the default load factor (.75) offers a good
// * tradeoff between time and space costs. */
2.主数组的长度为什么必须为2^n?
原因1:h & (length-1)与h % length操作等效;等效的前提是:length必须为2^n,
原因2:防止哈希冲突,位置冲突
验证:
length:8
hash 3 0000 0011
length-1 0000 0111
------------------------
0000 0011 -->3位置
hash 2 0000 0010
------------------------
0000 0010 -->2位置
以上操作说明hash为3与hash为2算出来的位置不一样,前提是length必须为2^n
如果现在length取9
length:9
hash 3 0000 0011
length-1 0000 0111
------------------------
0000 0000 -->0位置
hash 2 0000 0010
------------------------
0000 0000 -->0位置
说明如果length不是2^n,
hash(3)和hash(2)计算出来的位置一样,位置冲突就会加链表,效率低,因此主数组长度为2^n可以防止位置冲突。