第十一篇:Java中Map详解 HashMap、HashTable、LinkedHashMap、TreeMap、HashMap扩容原理详解

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的用法基本相似,我们这里说下不同

类型HashMapHashTable
是否线程安全
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,要不然会报错。

插入、删除、查找和红黑树是一模一样的,本文就不再叙述了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值