IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> Hashmap底层实现原理(JDK1.8) -> 正文阅读

[数据结构与算法]Hashmap底层实现原理(JDK1.8)

JDK1.8Hashmap实现原理

上一次我们已经了解了JDK1,7的HashMap的实现原理和红黑树,接下来我们看看JDK1.8的HashMap的实现原理

红黑树简介

jdk1.7HashMap的实现原理

在JDK1,8中,HashMap 数据结构为 数组+链表+红黑树.

因为当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)。因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。

在这里我们就讲一下jdk1.7和1.8的HashMap的区别吧:

hash()方法:

首先就是hash()方法,1.7版本的hash()方法的散列程度比较大,但是由于1.8引入了红黑树,所以1.8版本的hash()的散列程度相对于1.7来说比较小.

jdk1.8:

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

jdk1.7:

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();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

从上面可以看到JDk1.8中的hash方法实现简单得多.

put()方法:

我们再来看看put方法();

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)  //如果数组为空,那么就初始化这个数组;
            n = (tab = resize()).length;     //resize()对数组进行初始化
        if ((p = tab[i = (n - 1) & hash]) == null)   //如果这个元素所在数组对应的索引上没有元素的话,那么就直接new一个Node对象并赋值;
            tab[i] = newNode(hash, key, value, null);
        else {        //如果这个元素所在数组对应的索引上已经有元素;
            Node<K,V> e; K k;
            if (p.hash == hash && 
                ((k = p.key) == key || (key != null && key.equals(k))))  //如果数组该索引的第一个元素的key与要插入的key相同,那么直接将e对象等于p;
                e = p;
            else if (p instanceof TreeNode)  //如果p节点是树节点,即红黑树的情况时:
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //将节点插入到红黑树.
            else { //链表的情况 
                for (int binCount = 0; ; ++binCount) {    //遍历链表
                    if ((e = p.next) == null) {        /
                        p.next = newNode(hash, key, value, null);  //遍历结束后,将新的node节点插入到最后一个节点的后面(尾插法)
                        if (binCount >= TREEIFY_THRESHOLD - 1)  //TREEIFY_THRESHOLD 值为8;当bincount >= 7.即当第九个元素加入链表的时候,链表的长度大于8的时候,将链表转化为红黑树.
                            treeifyBin(tab, hash); //将链表转化为红黑树.
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))  //如果在链表中找到key相同的元素,直接退出循环
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //记录修改次数
        ++modCount;
        //判断是否需要扩容.
        if (++size > threshold) //threshold(阈值) = capacity(table.length(数组长度)) * 0.75(加载因子,默认是0.75)
            resize();  //扩容
        afterNodeInsertion(evict);
        return null;
    }

如果存在key节点,返回旧值,如果不存在则返回null.

我们可以看到jdk1.7的时候使用的链表插入是头插法(并发情况下可能会出现环形链表),1.8使用了尾插法;

基本上这里我们就已经将put方法的大致流程走完了,接下来我们看看如何将链表转化为红黑树:
treeifyBin()方法

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)  //MIN_TREEIFY_CAPACITY值为64;即如果数组为空或者数组的长度小于64的话不会将链表转化为红黑树,而是将数组扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) { //将链表转为红黑树
            TreeNode<K,V> hd = null, tl = null;
            do { 
                TreeNode<K,V> p = replacementTreeNode(e, null);   //将Node对象转化为TreeNode对象
                if (tl == null)   //然后将单链表转化为双向链表,hd是头结点;
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab); //将双向链表转化为红黑树;
        }
    }

replacementTreeNode()方法,将Node对象转化为TreeNode对象;

   TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

TreeNode对象的属性及有参构造:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // 指向前一个节点
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

treeify()方法:将双向链表转化为红黑树

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) { //遍历链表
                next = (TreeNode<K,V>)x.next;     
                x.left = x.right = null; //先将节点的左右节点都设置为null;
                if (root == null) {  //根节点
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) { //从根节点开始遍历红黑树;
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)  //不是直接比较的是值,比较的是哈希值
                            dir = -1;    //代表往左边走
                        else if (ph < h)
                            dir = 1;     //代表往右边走
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) || 
                                 (dir = compareComparables(kc, k, pk)) == 0) //查看key对象是否实现了Comparable<C>接口,如果实现了就用compareto()方法进行比较;
                            dir = tieBreakOrder(k, pk);//如果还相等的话就用compareto()方法比较对象的getClass().getName();如果还相等的话就比较两个对象的System.identityHashCode();
                            /*
                            简单理解就是:
                            先比较a,b的hash值,如果相等且实现了Comparable<C>接口
                            就比较a.compareto(b),如果还相等
                            就比较a.getClass().getName(). compareTo(b.getClass().getName())) 如果还相等
                            就比较System.identityHashCode(a) <= System.identityHashCode(b) 
                            */

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);  //红黑树调整的逻辑
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);  //将生成的红黑树的根节点root赋值给数组的index下的第一个元素;
        }

moveRootToFront()方法:
把红黑树的根节点设为其所在的数组的第一个元素
首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构
这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            if (root != null && tab != null && (n = tab.length) > 0) {
                int index = (n - 1) & root.hash;
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                if (root != first) {
                    Node<K,V> rn;
                    tab[index] = root;
                    TreeNode<K,V> rp = root.prev;
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    if (rp != null)
                        rp.next = rn;
                    if (first != null)
                        first.prev = root;
                    root.next = first;
                    root.prev = null;
                }
                assert checkInvariants(root);
            }
        }

下面总结来说一下put(key,value)的过程:

1. 判断键值对数组tab[]是否为空或为null,否则以默认大小resize();

2. 根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则进行下一步;

3. 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理:

  1. 如果为链表,那么就遍历链表,将元素插入到链表的最末尾(尾插法),并且判断链表的长度是否大于8,如果链表的长度大于8的话,先判断数组是否为空或者数组的长度是否小于64,如果小于64,则不用将链表转为红黑树,直接将数组扩容,如果链表长度大于等于64,那么就将链表转化为红黑树.
  2. 如果是红黑树,那么就将元素插入到红黑树中.

resize()扩容方法;

1.7和1.8它们判断扩容的条件有所不同:

jdk1.7:
在这里插入图片描述
jdk1.8:
在这里插入图片描述

让我们来看看1.8的扩容方法:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;    //初始化
        //开始扩容
        if (oldTab != null) {
            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 { // preserve order  对链表的元素扩容
                    /*
                    jdk1.7中对链表的节点是一个一个的移动;
                    jdk1.8中对链表的节点会将索引不变的成为一个新链表,对索引翻倍的形成另外一个链表,然后对整个链表进行移动.
                    */
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                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;   //扩容之后的元素要么在newTab[j]要么在newTab(j + oldCap);
                        }
                    }
                }
            }
        }
        return newTab;
    }

split()方法:红黑树的扩容;

 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);   //this:当前HashMap对象;newTab[]:新数组;j:数组的索引;oldCap:老数组的长度;
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)  // UNTREEIFY_THRESHOLD = 6; 当长度小于6的时候,还是链表
                    tab[index] = loHead.untreeify(map);
                else {          //当长度大于6时
                    tab[index] = loHead;
                    if (hiHead != null)  //高位链表不为空 
                        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);
                }
            }
        }
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-06-26 17:03:41  更:2022-06-26 17:03:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:46:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码