zoukankan      html  css  js  c++  java
  • hashmap详解(基于jdk1.8)

    简介:

    在jdk1.8中,hashmap有了较大的优化,底层实现由之前的“数组+链表”改为了“数组+链表+红黑树”。jdk1.8的hashmap的数据结构如图所示,当链表节点较少时仍然以链表形式存在,当链表节点较多时(大于8)会转化为红黑树。

    重要知识点:

    1、文章中头节点指的是table表上索引位置的节点,也就是链表的头结点

    2、根节点(root)指的是红黑树最上面的节点,也就是没有父节点的节点

    3、红黑树的根节点不一定是索引位置的头结点(链表的头结点),hashmap通过moveRootToFront方法来维持红黑树的根节点就是索引位置的头结点,但是在removeTreeNode方法中,当movable为false时,不会调用moveRootToFront方法,此时红黑树的根节点不一定是索引位置的头结点,该情形发生在hashIterator的remove方法中

    4、转为红黑树之后,链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转化为红黑树节点,链表结构就不存在了

    5、在红黑树上,叶子节点也可能有next节点,因为红黑树的结构与链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有next节点

    6、源码中的一些变量的定义为:如果定义了一个节点p,则pl(p left)为p的左节点,pr(p right)为p的右节点,pp(p parent)为p的父节点,ph(p hash)为p的hash值,pk(p key)为p的key值,kc(key class)为key的类等等。

    7、链表中移除一个节点只需要如图的操作

    8、红黑树在维护链表结构时,移除一个节点只需要如图所示操作(红黑树中增加了一个prev属性),其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作

    9、源码中进行红黑树的查找时,会反复使用两条规则:1)如果目标节点的hash值小与p节点的hash值,则向p节点的左边遍历;否则向p节点的右边遍历。2)如果目标节点的key值小与p节点的key值,则向p节点的左边遍历;否则向p节点的右边遍历。这两条规则是利用了红黑树的特性(左节点<根节点<右节点)

    10、源码中进行红黑树的查找时,会用dir(direction)来表示向左还是向右查找,dir存储的值是目标节点的hash/key与p节点的hash/key的比较结果

    基本属性:

    // 默认容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
     
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;    
     
    // 默认负载因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f; 
     
    // 链表节点转换红黑树节点的阈值, 9个节点转
    static final int TREEIFY_THRESHOLD = 8; 
     
    // 红黑树节点转换链表节点的阈值, 6个节点转
    static final int UNTREEIFY_THRESHOLD = 6;   
     
    // 转红黑树时, table的最小长度
    static final int MIN_TREEIFY_CAPACITY = 64; 
     
    // 链表节点, 继承自Entry
    static class Node<K,V> implements Map.Entry<K,V> {  
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
     
        // ... ...
    }
     
    // 红黑树节点
    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;    // needed to unlink next upon deletion
        boolean red;
       
        // ...
    }

    定位哈希桶数组索引位置

    由于HashMap的数据结构为“数组+链表+红黑树”的组合,所以我们希望这个HashMap里面的元素位置尽量分布地更加均匀,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。HashMap定位数组索引的位置,直接决定了hash方法的离散性能。源码为:

    // 代码1
    static final int hash(Object key) { // 计算key的hash值
        int h;
        // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    // 代码2
    int n = tab.length;
    // 将(tab.length - 1) 与 hash值进行&运算
    int index = (n - 1) & hash;

    1、拿到key的hashCode值

    2、将hashCode的高位参与运算,重新计算hash值

    3、将计算出来的hash值于(table.length-1)进行&运算

    解读:对于任意给定的对象,只要他的hashCode()返回值相同,那么计算得到的hash值总是相同的。把hash值对table长度取模运算,这样一来,元素的分布相对来说是比较均匀的。

    但是模运算的消耗比较大,计算机中的位运算比较快,因此使用代码2的位与运算来代替模运算。他通过(table.length-1)&h来获得该对象的索引位置。

    在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table的长度较小的时候,让高位也参与运算,并且不会有太大的开销。

    例子:

    当table的长度为16时,table.length-1=15,在二进制中,此时低四位全部为1,高28位全部为0,当0进行&运算时结果必然为0,因此此时hashCode与“table.length-1”的&运算只取决于hashCode的低四位,在这种情况下,hashCode的高28位就没起到任何作用,并且由于hash结果只取决于hashCode的低4位,hash冲突的概率也会增加。因此,在JDK1.8中,将高位也参与计算,目的是为了降低hash冲突的概率。

     get方法:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
     
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1.对table进行校验:table不为空 && table长度大于0 && 
        // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // 6.找不到符合的返回空
        return null;
    }

    4.如果是红黑树节点,则调用红黑树的查找目标节点方法 getTreeNode。

    final TreeNode<K,V> getTreeNode(int h, Object k) {
        // 1.首先找到红黑树的根节点;2.使用根节点调用find方法
        return ((parent != null) ? root() : this).find(h, k, null);
    }

    2.使用根节点调用 find 方法。

    /**
     * 从调用此方法的节点开始查找, 通过hash值和key找到对应的节点
     * 此方法是红黑树节点的查找, 红黑树是特殊的自平衡二叉查找树
     * 平衡二叉查找树的特点:左节点<根节点<右节点
     */
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
        // 1.将p节点赋值为调用此方法的节点,即为红黑树根节点
        TreeNode<K,V> p = this;
        // 2.从p节点开始向下遍历
        do {
            int ph, dir; K pk;
            TreeNode<K,V> pl = p.left, pr = p.right, q;
            // 3.如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
            if ((ph = p.hash) > h)
                p = pl;
            else if (ph < h) // 4.如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
                p = pr;
            // 5.如果传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if (pl == null)    // 6.p节点的左节点为空则将向右遍历
                p = pr;
            else if (pr == null)    // 7.p节点的右节点为空则向左遍历
                p = pl;
            // 8.将p节点与k进行比较
            else if ((kc != null ||
                      (kc = comparableClassFor(k)) != null) && // 8.1 kc不为空代表k实现了Comparable
                     (dir = compareComparables(kc, k, pk)) != 0)// 8.2 k<pk则dir<0, k>pk则dir>0
                // 8.3 k<pk则向左遍历(p赋值为p的左节点), 否则向右遍历
                p = (dir < 0) ? pl : pr;
            // 9.代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
            else if ((q = pr.find(h, k, kc)) != null) 
                return q;
            // 10.代码走到此处代表“pr.find(h, k, kc)”为空, 因此直接向左遍历
            else
                p = pl;
        } while (p != null);
        return null;
    }

     明天再写

  • 相关阅读:
    跨域导致FormsAuthentication.Decrypt报错:填充无效,无法被移除
    Php构造函数construct的前下划线是双的_
    DNN学习资源整理
    改进housemenu2使网站导航亲Seo并在新窗口中打开。
    推荐10款非常优秀的 HTML5 开发工具
    Ext.Net系列:安装与使用
    Devexpress 破解方法
    Microsoft Visual Studio 2010 遇到了异常,可能是由某个扩展导致的
    浮躁和互联网
    chrome 默认以 https打开网站
  • 原文地址:https://www.cnblogs.com/qumasha/p/12844569.html
Copyright © 2011-2022 走看看