拉克丝小声的在嘀咕: 天苍苍海茫茫,面试我很忙...
" 今天看你早早地准备去面试,又这么沮丧的回来,面试的不好? "
拉克丝十分生气地说:" 我学了这么多的知识,为啥面试以前的问题还问,现在都没人用了,他问我你对 JDK1.7版本的HashMap有什么了解,现在不是都用 1.8了,他居然还问我这个问题(深深叹了一口气)。
"来我给你讲一下JDK 1.7HashMap 去给我买杯 奈雪的茶"
" 你怎么还不去买呀"
"你给我掏钱呀?"
(我居然自己搬石头扎了自己脚),过了几分钟,拉克丝高兴地拿着奶茶回来了给我,我拿着手中的奶茶,奶茶再也不香了前言
接下来我们用 思维导图来分析一下HashMap
假如,我们现在已经把刘备放进了这个数组,这是我们再放进去一个 变量如下
但是这样也出现了一个问题
"你说的好复杂呀,怎么思维导图都这么复杂呢"
"其实也不是很复杂,来接下来带你看看源码,你就知道了"
1),首先我们先来看一下它的属性
//默认数组的容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认数组最大的限制
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子是 3/4=0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空的数组
static final Entry,?>[] EMPTY_TABLE = {};
//table 存的就是Entry 放的就是 k ,v
transient Entry[] table = (Entry[]) EMPTY_TABLE;
//每次put 添加数据的时候 +1 size()方法返回的就是size
transient int size;
//扩容界限 默认是 16*0.75=12
int threshold;
final float loadFactor;
//这个也很重要,这个与 ConcurrentModificationException 异常有关
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
2),接下来我们看下他的构造方法
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);
this.loadFactor = loadFactor;
//为什么把容值赋值给一个域值( threshold ) 呢?
threshold = initialCapacity;
//init() 方法是空的,实际上是在 LinkedHashMap上用,这里我主要说hashmap 这里不在继续深入了
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
3),接下来我们来分析一下 put() 方法
public V put(K key, V value) {
//判断当前数组是否已经初始化,类似于懒加载/延迟加载,当你put存储元素的时候,才进行初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry 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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
我们具体来看下inflateTable是怎么进行初始化的。
inflateTable(threshold);
方法为
private void inflateTable(int toSize) {
//翻译为: 找到一个 toSize的2的幂次函数
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建了一个 Entry对象长度为 capacty
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
注意:
1),不知道大家发现没有 inflateTable()传入的参数 toSize 就是 threshold
2),threshold 就是我们再构造器自己指定数组容量 ,如果没有指定那就是默认的容量16
问题:
1),为什么不是直接使用 toSize 来作为新数组的长度?
2),为什么要根据 toSize 来获取它的2的幂次函数来作为新数组的长度呢?
我们来看下 roundUpToPowerOf2(int toSize) 的源码 ,我们假设在创建数组的时候,指定容量为10,toSize = threshold =10;
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
注意:
1),number >= MAXIMUM_CAPACITY 10<最大容量,索引会直接走三目表达式的的false
2), Integer.highestOneBit((number - 1) << 1) 这个方法是roundUpToPowerOf2()方法的关键所在
那 Integer.highestOneBit() 这个方法是干什么的呢?
public static void main(String[] args) {
int i = Integer.highestOneBit(15);
int i1 = Integer.highestOneBit(16);
int i2 = Integer.highestOneBit(17);
System.out.println(i);
System.out.println(i1);
System.out.println(i2);
}
//运行结果为
8
16
16
从结果可以看出
:这个方法会根据你传入的数,得到一个小于等于这个数的2幂次方函数,例如你传入的是10,那么你得到的就是8
拉克丝一脸疑惑地问: 你刚才不是说 roundUpToPowerOf2() 方法返回的是大于等于2的幂次方函数? 然而在这个方法内部实现却是 Integer.highestOneBit() 方法返回却是 小于等于 2的幂次方函数,这样不就发生冲突了?
"这个问题都被你发现了,那为什么会这样呢?让我来给你讲一下 Integer.highestOneBit() 方法是如何实现的你就知道了"
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
例如我们现在传入的i=10
扩展:
8421
8421码: 例如 10的8421码,二进制表示为 0000 1010 , 这里结果为 8+2=10,如果高位有1,每次都是2倍递增。
|(或运算): 只要有一个为1,结果就为1。例如 0|0=0; 0|1=1;1|0=1;1|1=1;
方法执行1 =10如下步骤
i |= (i >> 1); 运行如下 >> 右移是从左边添加一个0
第一步右移一位
10的二进制 0000 1010
右移一位 0000 0101
|(或运算)
结果为 0000 1111
第二步 右移俩位
拿到第一步结果 0000 1111
右移二位为 0000 0011
|(或运算)
结果为 0000 1111
其实后面的都不用直接运算了,因为右移都是向高位补0,而低位一直都是1,所以结果一直都不会变了
这时,我们来看最后一句return 语句
i - (i >>> 1)
i值为 0000 1111
i >>> 1 0000 0111
-
结果为 0000 1000 即 8
因为我们传入的是10 所以它的最小2的幂次函数就是8 ,这不正是我们需要的结果呢
"你这里是特殊情况吧,比如我要传个很大的值,他的二进制不是应该很长,你这种情况不适用呢?拉克丝问道
"看来你还是不是太懂呢,那我在给你举个栗子你就懂了"
现在我就不举一个具体数字的例子来给你算一下了,我就直接用 * 来表示这是一个很长的数字
第一步左移1位
传入的值 0001 ****
右移一位后 0000 1*** (注意: 因为 0|1=1;1|0=1;1|1=1,所以取| 结果一定为1)
|(或运算)
运算结果 0001 1***
第二步右移2位
1结果值 0001 1***
右移2位后 0000 011*
|(或运算)
运算结果为 0001 111*
第二步右移4位
2结果值为 0001 111*
右移4位 0000 0001
|(或运算)
运算结果为 0001 1111
发现:
这里你有没有发现 这个算法就是在慢慢地,慢慢地把你的这些低位转化为1,那你为什么还要有右移8位,右移16位呢?
:因为我们是一个int类型,int类型占4个字节,每个字节8位 总共有32位。
拉克丝感叹地说"啊,JDK的作者已经对运算已经到了出神入化的地步了,我想都想不到"
这时我们回到 初始化table的方法中 Integer.highestOneBit((number - 1) << 1)
Integer.highestOneBit((number - 1) << 1)
例如 我们传入的是10 这时我们要得到一个2的幂次方数,值为16,按照这样的话 我们肯定要把10变大,取值范围为 16< 值 <32
这时我们不关心它-1,我们直接 把10左移一位
10 0000 1010
左移一位为 0001 0100 即20
例如 我们的后几位全是 0001 1111 后面全是1 结果是 31 也没有超出我们的范围
这里左移一位,直接判断他的值取值范围在 16 < 20 < 32,正是我们要的
那它为什么要 -1 呢?
: 这里是一种特殊的情况,例如 我们传入的是8,本来就是2的幂次方函数,执行这个方法返回的应该是8,如果不减1会出现下面这种情况
8的二进制 0000 1000
左移一位 0001 0000 结果为 16 与我们想要得到的结果不一致,所以要进行-1操作
8-1=7 的二进制位 0000 0111
左移一位 为 0000 1110 结果为12
找到最低的2的幂次函数 是8与我们想要的结果一致
那初始化数组完成后,我们现在继续看这个方法是如何计算索引的
put() 方法内部源码
int hash = hash(key);
int i = indexFor(hash, table.length);
//这是根据key计算出它的hashCode
final int hash(Object k) {
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);
}
//通过拿到的hashcode 和数组长度 算出数组下标
static int indexFor(int h, int length) {
return h & (length-1);
}
扩展
按位与& :两位全为1,结果才为1,例如 0&0=0;0&1=0;1&0=0;1&1=1
异或 ^ : 两个相应位为“异”(值不同),则该位结果为1,否则为0,例如0^0=0; 0^1=1; 1^0=1; 1^1=0;
疑问: 根据上面的思维导图是 hashcode的值 % 数组的长度来进行计算的,那么这里的这样一句代码是什么意思呢?
例如我们调用 indexFor()这个方法,随便传入一个hashcode 数组长度为16 进行如下计算
16-1 =15 的二进制位 : 0000 1111
随便写一个二进制 : 1010 1010
进行&操作
结果为 0000 1010 即10
从这个可以反映出,高位都是0,底位和这个hashcode的底层都是一样的,那么,我这个结果的取值范围就是你这个低四位的取值范围,而我这个hashcode本身就是随机的,是在 0000 -1111本身就是0-15之间的,正好符合我们的约束。
其实这是有一个规律的:
:就是它的高位都是0,低位都是1,如果说我们直接拿16来进行运算,结果将会怎么样呢?
16的二进制位 0001 0000
随机hashcode 1010 1010
进行&操作
结果为 0000 0000 即 0
注意: 即得到了结果为0,不符合我们的要求
这里我们反推一下,indexFor() 方法的length 一定要是2的幂次函数,才能和 h & (length-1) 进行配套使用,所以这里也解决了我们刚开始那个疑问,为什么要在创建数组长度的时候为什么一定要找一个大于等于2的幂次方函数,而不是直接使用我们指定的长度。
最后我们再来看一个问题
15的二进制: 0000 1111
Hash二进制: 0100 1010 (这里不管我们怎么改变hashcode 的高位,例如 1111 1010 ,1010 1010 等等)
进行&操作
结果为 0000 1010 即 10
得出结论: 不论我们如何改变这个hashcode的高位 ,最终都不会影响它的结果
hash() 计算出它的hashcode方法如下
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
例如 一个hashcode 0100 1010
向右移动4位 0000 0100
进行^操作
结果为 0100 1110
经过^运算,右移运算之后,^运算后的hashcode和原先 k.hashCode() 的方法所返回来的hashcode 值,高位也参加到这个运算中,这也就解答了我们第二个问题,为什么算出来的hashcode 直接拿出来之后为啥要进行右移运算(散列性)。
因为拿到它的hashcode 是根据Object.hashCode()来拿到的,因为他考虑到你可能重写它的hashCode()方法,那么很有可能,你的技术水平不行的话,你重写出来的hashCode()方法,是有问题的,从而导致调用你重写的hashCode()返回的值,非常不均匀,那么它通过右移这些方法,可以去容错,这一点在jdk1.8会有一些优化。
"好了,到这里讲的听懂了?"
"有那么一点点感觉,我已经拿我的小本本给记下了,我这边再问你一个问题,那为HashMap什么要使用链表呢?"
"因为不管你是通过hashcode()算法 还是 indexFor() 方法来最终算出它的数组下标都有可能是重复的,这时就发生了冲突,这被称为Hash冲突,而解决Hash冲突只有俩个办法 1),就是我们这里说的链表法 2),就是再散列法,如果算出来的索引和旧的索引发生冲突,那么我就在次计算一个新的索引,直到没有发生重复为止。"
当我们的key为null的时候,put()方法是如何做的呢?
if (key == null)
return putForNullKey(value);
private V putForNullKey(V value) {
for (Entry 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;
}
"我接下来讲的也很重要,来我们再看下put方法是如何解决下面这个问题的"
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("1", "1");
String value = map.put("1", "2");
System.out.println(value);
System.out.println(map.get("1"));
}
运行结果为:
1
2
for (Entry 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;
}
}
我们接下来在put()方法往下面看具体代码
//把k,v放进数组中去
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否扩容
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);
}
//具体添加到HashMap 的方法
void createEntry(int hash, K key, V value, int bucketIndex) {
// 获取链表节点的 Entry
Entry e = table[bucketIndex];
//把新的节点插在了链表的头部
table[bucketIndex] = new Entry<>(hash, key, value, e);
//+1 表示数组个数
size++;
}
"那为什么hashMap需要进行扩容呢?"
addEntry() 方法
resize(2 * table.length);
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 e : table) {
while(null != e) {
Entry 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;
}
}
}
那我们应该怎么在多线程情况下,避免hashmap的死循环链表呢?
transfer() 数组转移 还有,一个判断我一直没有说如下
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//值默认为 0
transient int hashSeed = 0;
// capacity 值就是新数组容量的长度
final boolean initHashSeedAsNeeded(int capacity) {
//currentAltHashing 默认就是false
boolean currentAltHashing = hashSeed != 0;
//sun.misc.VM.isBooted() 判断JVM是否启动 当然一般都是启动的呢 true
//Holder.ALTERNATIVE_HASHING_THRESHOLD 值为 Integer.MAX_VALUE
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//switching 因为 currentAltHashing 为false
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
这里我们可以看出来
: switching = false ^ useAltHashing 当useAltHashing 我们可以知道 只有当capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 时useAltHashing 才为true 才能 运行
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
但是我们有知道了 Holder.ALTERNATIVE_HASHING_THRESHOLD 是Integer 的最大值 ,所以我们怎么用都不会出现 为true的情况,所以 if(rehash) 方法里面的代码一直都不会执行
"可学到了,给你讲了这么多有点饿了,我去找点吃的,你先理解一下,故事还没有结束哦"
"HashMap 数组 , 链表" 拉克丝看起了笔记默默地念叨