子龙带你读源码之HashMap

158 阅读11分钟

在下文中,你将学到:

  • HashMap源码的各个参数和方法的含义以及简要原理
  • HashMap的为什么要选用红黑树作为他的扩容方式,为啥不是AVL,二叉搜索树等,他们的优缺点比较
  • 带你利用java纯手写一个红黑树

一.HashMap的特点(数组 + 链表 + 红黑树)

---- 1.HashMap继承于如下接口

public class HashMap<K,V> extends AbstractMap<K,V>    
implements Map<K,V>, Cloneable, Serializable {

---- 2.一个最简单的Hashmap构成:数组 + 链表

ps:我个人喜欢叫这种哈希表为拉链法


那么问题来了:

1)问题一:既然是数组,那么数组初始化了,那么申请的内存有多少,初始化了多大的长度?

      源码就是天然的技术文档,看源码喽。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 1左移4位,变为1000,初始化为16

2)问题二:这个数组可以申请的最大长度是多少呢?

static final int MAXIMUM_CAPACITY = 1 << 30; // 数组初始化的最大长度接近int的临界值1 << 31

ps这里有这么一个常量是啥意思呢:static final float DEFAULT_LOAD_FACTOR = 0.75f; // 缺省的负载因子的大小

负载因子?

负载因子是用来计算负载容量(所能容纳的最大Node个数)的,当前list长度 length,负载因子 loadFactor

负载容量的计算公式为: 

threshold = length * loadFactor

默认负载因子为 0.75。也就是说,当Node个数达到当前list长度的75%时,就要进行扩容,否则会增加哈希碰撞的可能性。负载因子的作用是在空间和时间效率上取得一个平衡。

float DEFAULT_LOAD_FACTOR = 0.75f

3)问题三:上面提到了红黑树,那什么时候用红黑树呢?


首先我们要知道为啥要使用红黑树,如果采用链表可能会出现如上情况,当我们插入数据的时候会通过计算它的hashcode之后把它插入到某个数组节点下方,然后一直拉链。导致这个拉链很长很长,然后你在拿这个值的时候就会变为On的复杂度,违背了hashmap接近O1复杂度的原则,于是乎,红黑树诞生了。红黑树的优势在哪咱们在下面讲。

---- 这里又有一个问题:链表啥时候会转换成红黑树?

static final int TREEIFY_THRESHOLD = 8; /**树化阈值*/ 
static final int MIN_TREEIFY_CAPACITY = 64; /**所有的元素超过64, 链的长度超过8,会tree话*/

源码里面说的很明确,当hashmap里面所有元素超过64个,某个链的长度超过8,此时才会tree化

---- 注意当你删除hashmap里面的元素,导致单颗树的元素 <= 6时,此时单颗树就会再一次变为链表

static final int UNTREEIFY_THRESHOLD = 6;/**树降级成为链表的阈值*/

4)问题四:上图中链表的结点的构成是啥呢?

Node(int hash, K key, V value, Node<K,V> next) { // 链表的基本知识
    this.hash = hash;  //保存当前节点的hash值  
    this.key = key;    // key,value保存结点信息
    this.value = value;    
    this.next = next; // 保存结点的下一个结点的位置,以达到拉链的目的
}

5)问题五:hashmap的初始化数组的方法

/** 作用:返回一个大于等于当前cap的一个数字,并且这个数字一定是2的次方数* cap = 3 = 11
* 3 - 1 = 2 = 10
* 10 >>> 1 = 01 | 11 = 11 ;11 >> 2 = 00 | 11 = 11;11 >> 4 = 00 | 11 = 11;
* 11 >> 8 = 00 | 11 = 11;11 >> 16 = 00 | 11 = 11;
* 11 + 1 = 100 = 4 所以其实真正的初始化的数组长度为4
* 所以 数组初始化的时候其实就是把cap的二进制位都变为0然后左移一位拿到初始化长度
static final int tableSizeFor(int cap) {    
    int n = cap - 1; /**不减一的话会得到一个想要的数 * 2的一个大小*/    
    n |= n >>> 1;   
    n |= n >>> 2;   
    n |= n >>> 4;
    n |= n >>> 8;    
    n |= n >>> 16;    
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

6)问题六:hashmap的put方法,get方法, remove方法

ps:下文提到了哈希的计算,这里提前介绍一下hash的计算(扰动函数)

 * 作用: 让key的hash值的低16位与高16位参与运算 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

右位移16位,正好是32bit的一半(int 是32位的),自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也变相保留下来。


public V get(Object key) {
    Node<K,V> e;// 存的时候hash了一下,所以这里要拿到hash之前的数据
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {// tab 引用当前hashMap的散列表
 // first :桶位中的头元素    
 // e:临时node元素    
 // n:table数组长度
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 第一种情况:定义出来的桶位,即为咱们要get的数据
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 说明当前的同为不只一个元素,可能是链表也可能是红黑树
        if ((e = first.next) != null) {
            // 第二种情况,桶位升级成了红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 第三种情况,桶位拉链了
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    /**     * tab 引用当前hashmap的散列表
     * p 表示当前散列表的元素
     * n 表示散列表数组的长度
     * i 表示路由寻址 结果     
*     * */
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中最耗费内存的散列表
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //最简单的一种情况,寻址找到的位置刚好是null, 直接将当前节点扔进去就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // e:node临时元素(不为null,找到一个与当前要插入的key val 一致的key)
        // k:表示临时的一个key
        Node<K,V> e; K k;
        // 表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode) // 红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 链表的情况,而且链表的头元素与我们要插入的key不一致
            for (int binCount = 0; ; ++binCount) {
                // 说明迭代到最后一个元素了,也没找到一个与你要插入的key一致的node,
                // 说明要加入到当前链表的末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 条件成立的时候,达到树化的标准
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 树化 
                       treeifyBin(tab, hash);
                    break;
                }
                // 条件成立的话,说明找到了相同key的node元素,需要进行替换操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // e不等于null, 条件成立, 找到了一个与你插入元素key完全一致的数据,需要进行替换
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // modCount 表示散列表结构被修改次数, 替换Node元素的value不计数
    ++modCount;
    // size表示散列表中node数量, 插入新元素,size自增,如果大于阈值,触发自增
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    // tab:引用当前hashMap中的散列表
    // p:当前node元素
    // n:表示散列表数组长度
    // index:表示寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) { // p计算hash位置
        // 说明路由的桶位是有数据的,需要进行查找操作,并且删除
        // node表示查找到的结果
        // e:表示node的下一个元素
        Node<K,V> node = null, e; K k; V v;
        // 第一种情况,当前桶位中的元素即为你要删除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 说明当前桶位要么是链表要么是红黑树
            if (p instanceof TreeNode) // 判断当前桶位是否升级为红黑树了
                // 第二种情况
                // 红黑树的查找操作,看我的红黑树的教程
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 第三种情况
                // 链表的情况
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 判断node不为空的话,说明按照key查找到需要删除的数据了
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 第一种情况:node是树节点,说明需要树节点移除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 第二种情况:桶位元素即为元素查找结果,则将该元素的下一个元素放到桶位中
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

7)问题七:为什么要扩容?是不够放了吗?No,一个很重要的原因是为了解决哈希冲突导致的拉链化严重,导致查询变为On,扩容可以解决该问题

final Node<K,V>[] resize() {
    // oldTab:引用扩容前的哈希表
    Node<K,V>[] oldTab = table;
    // oldCap:表示扩容之前的table数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // oldThr:表示扩容之前的扩容阈值,触发本次的扩容的阈值
    int oldThr = threshold;
    // newCap: 扩容之后的table大小
    // newThr:扩容之后,下次再触发扩容的条件
    int newCap, newThr = 0;
    // 条件如果成立说明 hashMap中的散列表已经初始化过了,这是一次正常的扩容
    if (oldCap > 0) {
        // 扩容之前的table数组大小已经达到 最大阈值了,则不扩容, 扩容条件设置为int最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // oldCap左移一位实现数值翻倍,并且赋值给newCap 小于数组最大值限制且扩容之前阈值 >= 16
        // 这种情况下,则下一次的阈值等于当前的阈值翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }    // oldCap = 0,说明hashMap中的散列表是null
    // new hashMap(initCap, loadFactor);
    // new Hashmap(initCap);
    // new HashMap(map); 并且这个map有数据
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr; // 说明newCap一定是2的次方
    // new HashMap() oldcap = 0, oldThr = 0
    else {
               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  // 0.75 * 16 = 12
    }
    // 传递的小于16的时候就为0了, newThr为0时,通过newCap和localFactor计算出一个newThr
    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;
    // 说明hashMap本次扩容之前,table不为null
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            // 当前Node节点
            Node<K,V> e;
            // 说明当前桶位中有数据,但是数据具体是单个数据,还是链表,还是红黑树,并不知道
            if ((e = oldTab[j]) != null) {
                // 方便JVM GC时回收
                oldTab[j] = null;
                // 说明没有拉链,直接根据哈希值和数组长度计算出当前元素的位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 第二种情况:当前节点已经tree化
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 桶位已经形成链表
                // 低位链表:存放在扩容之后的数组的下标位置,与当前数组的下标值一致
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    // 高位链表:存放在扩容之后的数组下标位置,为当前数组下标 + 扩容之前数组长度
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // hash -> .... 1 1111
                        // hash -> .... 0 1111
                        // & 1 0000
                        // 如果链表中的高位与1 得1 则放在高位链中
                        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;
                    }
                }
            }
        }
    }
    return newTab;
}