JDK1.7Hashmap底层实现原理(put方法):
在JDK1,7中,HashMap 数据结构为 数组+链表.
三句话,说清它的数据结构:
1.整体是一个数组;
2.数组每个位置是一个链表;
3.链表每个节点中的Value即我们存储的Object;
JDK1.7Hashmap的属性信息:
//Hashmap的初始化大小,初始化的值为16,1往右移4位为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap是动态扩容的,就是容量大小不能大于 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的扩容因子,这个值可以通过构造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空数据,默认构造的时候赋值为空的Entry数组,在添加元素的时候
//会判断table=EMPTY_TABLE ,然后就扩容
static final Entry<?,?>[] EMPTY_TABLE = {};
//表示一个空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//Hashmap的大小
transient int size;
//threshold表示当HashMap的size大于threshold时会执行resize操作。
//DEFAULT_INITIAL_CAPACITY=16
//扩容的阈值
int threshold;
//扩容因子,没有传入就使用默认的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;
//数据操作次数,用于迭代检查修改异常
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
初始化HashMap:
提供了有参构造和无参构造:
1.无参构造中,容器默认的数组大小 initialCapacity 为 16;
2.,有参构造中,指定数组长度时,如果指定数组的长度为9,但是真正在初始化时创建数组的长度不是为9而是16。
创建数组是在使用put方法时进行初始化创建。
public V put(K key, V value) {
//判断数组是否存在
if (table == EMPTY_TABLE) {
//如果不存在创建数组
inflateTable(threshold); //threshold如果是无参构造就是16,如果是有参构造就是传进来的数字.
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);//创建数组下标
//遍历判断key是否有重复,重复就覆盖
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;
}
}
modCount++;
//添加entry对象,进入addEntry之后会有判断扩容,两倍扩容,之后还得是2^n
//数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈(yu)值 (数组的长度乘以加载因子的值),因为加载因子默认0.75,所以要想不扩容就设为1,下标就不会大于yu值。
// 2.底层数组的bucketIndex坐标处不等于null
addEntry(hash, key, value, i);
return null;
}
进入inflateTable()方法:
private void inflateTable(int toSize) {
// 通过roundUpToPowerOf2方法获取大于toSize最近的2次方数
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//最终创建entry对象的数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
进入roundUpToPowerOf2()方法;这里我们可以看到返回的是大于number(number是)最近的2次方数
举个例子吧,如果我 new HashMap<>(9),那么这里的number就是9,返回的就是第一个大于等于9的2次方数也就是16.
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;
}
我们再来看看Integer的highestOneBit()方法吧:
highestOneBit()返回的是小于等于i的最近的二次方的数
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 = 17 举个例子吧:
随便举一个例子:
这个的思路是什么呢? 比如我们要一个 742 二进制就是 0010 1110 0110 的最近的二次方的数 我们只需要吧第一个1后面的数字全部变为0就行了,就是 0010 0000 0000,那怎么去完成呢?我们只需要将 0010 1110 0110变成 0011 1111 1111,然后将它右移一位变成0001 1111 1111,然后相减就得到了 0010 0000 0000,即就是离它最近的且小于等于它的二次方的数了。那为什么要右移那么多次呢?因为int类型是32位的
hash()方法:
final int hash(Object k) {
int h = hashSeed;//默认为0
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
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 >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
当我们对hashMap进行添加操作时:
比如:
我们给hashMap添加一个key为"123",value为"123"的一个元素:
hashMap.put(“123”,“123”);
首先通过key.hashcode()方法去获取到key的哈希值;
“123”.hashCode(); //值为48690
很显然用48690作为数组的index很不合理;因此使用了一个indexFor()方法
static int indexFor(int h, int length) { // h 为key 的 hash值;length 是数组长度
return h & (length-1);
}
简单来说就是用获取到的哈希值对数组的长度进行取余.
int index = key.hashcode % length ;
这时我们就可以把这个元素(“123”,“123”)(其实这里是一个Entry对象,这里先当做是一个元素,后边会讲到)放在索引为 index 的数组里面了.
但是这有会出现一个问题:
假如"123" 与 “1234” 的 哈希值 % length 一样;即
indexFor(“123”.hashcode()) == indexFor(“123”.hashcode());
当我们再对 hashMap 添加一个key为"1234",value为"1234"的一个元素:
hashMap.put(“1234”,“1234”);
我们就会发现(“1234”,“1234”) 与 (“123”,“123”)在数组的索引一样;这时我们就要加入链表来进行操作;
在这里有两种插入方式 : 头插法与尾插法
因为这是一个单向链表,所以查询时只能从头结点开始向下查询.为了插入的效率,采用头插法的方式.
如果要用尾插法的话,必须要遍历整个链表,扎到尾结点,然后让尾结点的next 等于 新加的元素;而头插法只需要将新加的元素的next节点 等于 当前链表的头结点 .每次插入头结点后都会将数组中的头结点引用替换覆盖成最新头结点的引用.(!!这个不能缺少,不然的话就没办法找到这个链表的头结点)
put有返回值,当map中插入值时,会去遍历链表,查找是否存在当前key,不存在当前存入的key时,返回null,存在时返回已存在key的value,并且会用当前value将其覆盖,保证key value一一对应,有人会说既然都要遍历为什么还要头节点插入,因为这里遍历链表查找是否有已存在的key,当找到之后就不再遍历还未遍历的,头插效率还是比尾插高。
//遍历判断key是否有重复,重复就覆盖
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; //如果有重复就返回之前的value值,如果没有就返回null;
}
}
然后来看一看,数组中存储对象Entry:
它继承了map.Entry接口,它有4个属性
key , value , next , hash.
数组,链表都]存储的Entry 对象.由于它是一个对象,所以在堆中进行存储,HashMap也是一个对象,也在堆里面进行存储,所以HashMap中的数组与链表实际上存储的是Entry对象的地址引用.
当put的key为null的时候:
if (key == null)
return putForNullKey(value);
putForNullKey()方法:
因为有些key的indexFor(key) == 0;所以数组的索引为0的位置不只是只有key为null的Entry对象.
当存储一个key为null的值的时候我们要先遍历链表,如果里面已经存储了key为null的Entry对象,我们就把key为null的Entry对象的value更换,并且将之前的value进行返回,如果没有存储key为null的Entry对象,那么就使用头插法将该Entry对象插入.(其实看源码我们发现这时候还没有生成Entry对象,只是将value传入addEntry()方法,在这个方法里面将生成Entry对象,并且在addEntry()方法里面,可能会出现数组扩容).
最后我们来看看addEntry()方法:
size表示的是整个HashMap的大小也就是HashMap存储了多少个元素.
threshold(阈值) = table.length(数组长度) * 0.75(加载因子,默认是0.75)
null != table[bucketIndex]; buckindex表示的是这个元素要插入数组的索引,如果这个索引上面没有元素,即使HashMap的大小大于阈值,也不发生扩容.
如果满足了扩容的条件,那么就进行resize()方法进行扩容.
void resize(int newCapacity) {
//把原哈希表数组赋值给oldTable
Entry[] oldTable = table;
//把原哈希表容量赋值给oldCapacity
int oldCapacity = oldTable.length;
//如果当前的哈希表容量已经达到允许的容量最大值(2的30次幂),则不再进行扩容
//且把当前哈希表的负载门槛设置为Integer的最大值。返回,跳过。
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新的哈希数组,容量为新传入的容量值
//该容量值必须是2的n次幂,且大于原数组容量大小
Entry[] newTable = new Entry[newCapacity];
//开始把原哈希表数组数据转入新创建的哈希表数组中
//在开始转存前要先根据新的数组容量及相应的算法得出是否使用哈希干扰掩码
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//转存完成后把新表内容放到HashMap的哈希表值中
table = newTable;
//设置当前容量下的负载门槛
//(新容量 * 负载因子)的值与(HashMap允许的最大容量(2的30次幂)+1) 进行比较,
//取值小的那一个
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer()方法
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
// 头插法数据拷贝
do {
// 获取下一元素
Entry<K,V> next = e.next;
// 计算元素在新数组中的索引
int i = indexFor(e.hash, newCapacity);
// 设置元素的后后续数据
e.next = newTable[i];
// 在计算出的索引位置放入元素
newTable[i] = e;
// 指向下一元素
e = next;
} while (e != null);
}
}
}
在这里我们可以看到扩容后,链表的顺序反了.
之后我们将table 等于 扩容后的数组;
然后重新计算阈值的大小.
扩容的目的其实就是为了将链表的长度变短
在多线程的情况下:
在多线程的情况下,有可能会让数组某个索引上的链表成为一个循环链表,会导致在get或者put的时候产生死循环.(主要还是由于头插法导致的).