HashMap底层简单原理
首先应该明白HashMap在jk7是数组加链表结构,
jk8是数组加链表结构,当链表长度大于8且底层数组大小超过64时,链表转为红黑树。
jk7链表采用的时头插法(多线程会产生循环链表),jk8采用的是尾插法。
无论jk7还是jk8,HashMap底层数组大小都是2的阶乘倍数,扩容也是在原来基础上变成两倍。
第一种方式:
HashMap<Integer, String> map = new HashMap<>(); //空构造器
/*
DEFAULT_LOAD_FACTOR = 0.75f 默认的加载因子(扩容阈值)
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
调用默认构造只会初始化默认的加载因子为0.75f,此时不会初始化数组,初始化数组是在put操作时进行的。
当进行put操作时,会判断底层数组table是否为空,如果为空,数组长度默认为16,此时扩容阈值为12。
HashMap<Integer, String> map = new HashMap<>(); //初始化默认的加载因子为0.75
map.put(1,"1"); //put第一个元素时,数组初始化,长度为16,放到数组下标为1的位置
map.put(17,"17");//put第二个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第一个元素后,下标也是1
map.put(33,"33");//put第三个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第二个元素后,下标也是1
map.put(49,"49");//put第四个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第三个元素后,下标也是1
map.put(65,"65");//put第五个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第四个元素后,下标也是1
map.put(81,"81");//put第六个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第五个元素后,下标也是1
map.put(97,"97");//put第七个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第六个元素后,下标也是1
map.put(113,"113");//put第八个元素时,hash后位置和第一个元素相同,此时形成链表结构,挂在第七个元素后,下标也是1
//put第九个元素时,hash后位置和第一个元素相同,此时链表长度从第二个元素算起已经7个,但是数组长度小于64,此时数组会扩 容至32,元素重新hash计算其在数组中的下标位置。
map.put(129,"129");
第二种方式(底层源码具体的实现方式,自己去看)
//此时有参构造放入的是阈值,此时底层会对这个值进行计算,得出最后数组的长度和阈值
//如果是3,计算后数组长度是4,阈值是3
//如果是5,计算后数组长度是8,阈值是6
//如果是8,计算后数组长度是8,阈值是6
//如果是9,计算后数组长度是16,阈值是12
//所以得出结论: 参数是2的倍数时,数组长度和传入参数相同,否则是该参数后面的2的最近的一个倍数
HashMap<Integer, String> map = new HashMap<>(3);
/**
* DEFAULT_LOAD_FACTOR = 0.75f 默认的加载因子
* initialCapacity : 扩容阈值
*
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
int num = 64;
HashMap<Integer, String> map = new HashMap<>(num); //初始化数组长度为64
for(int i = 1;; i++) { //死循环
int key = 1 + num * (i - 1);
map.put(1 + num * i, key + ""); //向数组中添加会产生hash冲突的元素
}
//当添加到第九个元素时,此时链表长度等于7,数组长度等于64,此时链表转为红黑树结构。
HashMap底层用了大量的位运算,位运算比用运算符的速度快非常多,因为HashMap结构经常改变,如果采用运算符的方式计算,那么速度相当的慢。
为什么hashMap数组长度是2的阶乘倍数?
因为底层是一个数组,为了减少hash冲突,使元素分散均匀,在进行&(与)运算时,2的阶乘倍数的二进制数减一就全是1,此时与运算就不会改变其二进制本身的值,举例:
16的二进制减一 就是15,和一个二进制为1100 1010进行与预算,相同为1,不同为0,所以其实就是将该值对16进行取模运算,不会改变二进制低位的值,假设如果不是2的阶乘倍数,则减一后二进制就不是全是1。假设为13
1100 1010 1100 1010
15 0000 1111 13 0000 1100
--------------- ----------------
0000 1010 (不改变低位的值) 0000 1000 (改变低位的值)
为什么hashMap扩容阈值是0.75?
根据泊松分布,0.75的扩容机制,对时间和空间来说是最合理的。阈值太小,浪费空间,阈值太大,时间来不及。
为什么jk7采用头插法,jk8采用尾插法?
头插在多线程时会产生循环链表的情况,尾插法不会。
头插法即插入链表的新元素替换原来位置的旧元素,旧元素放在新元素的后面,尾插法,就是新元素直接放在最后一个旧元素的后面。
hashtable 和hashMap的区别
hashMap允许key为null,如果key为null,则其hash值为0,默认放在首位。hashMap线程不安全。
hashtable不允许null值,会抛空指针。hashtable线程安全,因为put操作加了synchronized锁。