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;
}

 明天再写

posted @ 2020-07-20 23:59  qumasha  阅读(211)  评论(0编辑  收藏  举报