1、Map是什么?
Map中存放的数据是以key-value键值对存在的。通过Map我们可以用key来获取对应的value。Map也就做散列表和Hash表。
2、HashMap
HashMap的底层原理是哈希表。
我们都知道哈希表会存在Hash冲突,对待Hash冲突一般有两种方式:
1、线性探测法:
插入元素:当出现冲突时,依次往后遍历,如果出现空槽,则将值插入。
查找元素:首先找到hash位置,然后比较,如果不相同,则往后遍历比较,如果碰到空槽,则说明该值不存在
缺点:删除效率不高,需要将后面的元素挪动到前面。(PS:如果不挪动,可能会造成其他元素将当前空槽当作查询终点,造成查询失败)
同一个Hash值的元素扎堆,导致冲突严重,每次插入都需要遍历很久
2、拉链法
拉链法是HashMap采用的方法,比线性探测法方便很多
插入元素:当出现冲突时,对那个槽位拉起一个链表,将其放到链表的尾部
查找元素:找到槽位对应的链表,遍历查询即可。
HashMap原理:
HashMap的拉链法又进一步做了改进,当链表中节点的个数大于等于8并且table的长度大于等于64时,会将链表优化为红黑树,进一步加快查询和删除效率。
插入元素
首先要确定的是对象在table中的位置
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key.hashCode()实际是上返回对象的物理地址,每个对象都不一样。
hashcode是int类型,取值在-2147483648到2147483647,内存中肯定无法容忍这么大的数组。所以要对hashcode对n取模,作为其在数组中的位置。
n为2的n次幂时,位运算和取模运算结果一样的,但是位运算 (n-1)&hashcode的效率却高不少。
这也说明了hashcode在table中的位置主要与低位有关。
那为什么还要与hashcode的低16位异或呢?
将hashcode的低16位与高16位混合,让低位中融合了高位的信息,进一步加强了hashcode中低位的随机性,进一步减少hash冲突。
找到位置后,就看对应的为链表还是红黑树了。
如果是链表就遍历链表,如果之前没有插入过就放到链表尾部,如果插入过,就更新其value。
如果是红黑树,就是利用红黑树的二叉搜索性快速找到对应的位置,如果存在对应的key,就更新value,如果不存在,就插入。
这里特别说一下,红黑树是按照key的hash()方法返回的值排序的。
这里判断是否插入过的逻辑如下:
1、如果hash()值不同,对象肯定不同
2、如果equals()相同,返回该对象
3、如果hash()值相同,但是equals() == false的话,会先遍历左子树,如果在左子树找不到对应的节点,就搜索右子树。
HashMap扩容原理
扩容的时机:
1、当首次插入元素的时候,当HashMap初始化的时候,table是没有初始化的,当插入元素的时候,才会初始化。
2、当某个链表中的节点个数大于等于8,将要升级为红黑树的时候,但是如果table长度小于64会进行扩容代替升级为红黑树。
3、当Map中的对象个数超过阈值THRESHOLD时,也会触发扩容
//LOAD_FACTOR是负载因子,默认是0.75,也可以在初始化的时候自己指定
//CAPACITY是TABLE的长度。
THRESHOLD = LOAD_FACTOR * CAPACITY
如果table的长度大于1<<30就不再扩容
正常的扩容直接扩到两倍。
然后开始将数据从老table搬到新table。
代码如下:
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//如果只有一个节点
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//如果是红黑树
//处理方法和下面链表类似,只不过如果扩容后的数量仍大于界限,重新生成红黑树,具体代码见下方
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 如果是链表
//扩容后仍然在老位置j上
Node<K,V> loHead = null, loTail = null;
//扩容后到了新位置j+oldCap
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果与oldCap &运算为0
// 10101 e.hash
// 1000 oldCap &结果为0 ,
// 0111 oldCap-1 &结果为 101,
// 01111 newCap-1 &结果仍为101,还是在老位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//如果与oldCap &运算不为0
// 11101 e.hash
// 1000 oldCap &结果为1000,不为0
// 0111 oldCap-1 &结果为 101,
// 01111 newCap-1 &结果为1101 等于 101 + 1000,也就是101 + oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//和之前链表统计类似
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
//如果留在老位置的节点数量小于界限,就见红黑树转化为链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//如果留在老位置的节点数量大于界限,就转化为红黑树
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
//如果到新位置的节点数量小于界限,就见红黑树转化为链表
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
//如果留到新位置的节点数量小于界限,就见红黑树转化为链表
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
3、HashTable
HashTable和HashMap的用法基本相似,我们这里说下不同
类型 | HashMap | HashTable |
---|---|---|
是否线程安全 | 否 | 是 |
hash冲突解决方法 | 小于8 链表 ;大于8红黑树 | 只有链表 |
扩容机制 | 2倍 | 2倍+1 |
是否能插入null | 能 | 不能 |
HashTable由于效率低,已经不再推荐使用了,被ConcurrentHashMap替代了。
4、LinkedHashMap
从名字就可以看出LinkedHashMap实在HashMap的基础上改造的。
LinkedHashMap和HashMap有什么区别呢?LinkedHashMap中遍历是按照插入元素的顺序输出的。
原理:
LinkedHashMap继承了HashMap,LinkedHashMap内部维护了一个双向链表来依次保存插入的元素,实际上就是维护了一个head和tail节点。
当插入元素的时候,会在tail后插入元素,然后将tail指向插入的新元素
当删除元素的时候,会找到待删除元素的指针,因为链表是双向的,不需要再在遍历链表找寻该节点的前继节点,直接通过其before指针就ok了,删除也十分高效
具体代码如下:
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
5、TreeMap
TreeMap的底层就是完全的红黑树了。
用户可以通过指定自定义的Comparator来对key进行排序。如果没有指定Comparator的话,就按照key的类默认的Comparable来排序。如果是自定义的类一定要继承Comparable,要不然会报错。
插入、删除、查找和红黑树是一模一样的,本文就不再叙述了。