HashMap源码深度解析(深入至红黑树实现)以及与JDK7的区别四万字

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码深度解析(深入至红黑树实现)以及与JDK7的区别四万字相关的知识,希望对你有一定的参考价值。

基于JDK1.8对HashMap集合的主要方法源码解析,深入至底层红黑树的源码,并且与JDK1.7的HashMap做了比较全面的对比,最后给出了比较完整的HashMap的数据结构图!

本文主要是对JDK1.8的HashMap的主要方法实现做了分析,对于一些基础的知识,认为大家在看这篇文章之前是都懂得的,比如哈希表的原理、红黑树的原理!

如果大家有不了解这些原理的一定要去看看相关文章,否则如果直接看下面的源码的分析肯定有你看不懂的!

数据结构—红黑树(RedBlackTree)的实现原理以及Java代码的完全实现,这是看懂HashMap底层红黑树源码必备的基础知识!

数据结构—散列表(哈希表)的原理以及Java代码的实现,HashMap就是一张散列表,这是关于散列表的介绍!

文章目录

1 HashMap的概述

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

HashMap,来自于JDK1.2的哈希表的实现,JDK1.8底层是使用数组+链表+红黑树来实现的(JDK1.7是使用数组+链表实现的),使用“链地址法(拉链法)”解决哈希冲突。

实现了Map接口,存放的自然是key-value形式的数据,拥有Map接口的通用操作。允许null键和null值,元素无序。

实现了Cloneable、Serializable标志性接口,支持克隆、序列化操作。

此实现不是同步的,可以使用Collections.synchronizedMap()方法获得一个同步的Map。

默认容量为16,第一次存放元素时初始化;默认加载因子为0.75;扩容增量为增加原容量的1倍,即变成原容量的两倍。

2 主要类属性

主要类属性包括一些默认值常量属性,还有一些关键属性。

从这些属性可知,默认初始容量16,最大容量2^30,加载因子0.75。

链表树形化阈值8(大于),哈希表树形化阈值64(大于等于),resize时树还原阈值6(小于等于)。

/**
 * 默认初始容量为16,所有的容量必须是2的幂次方,这在哈希算法中会使用到。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * 最大容量为1 << 30,即2^30次方。
 * 我们知道int类型的范围是[-2^31 ~ 2^31-1],因此这里的2^30实际上就是int范围类的最大的2的幂次方值。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认加载因子为0.75
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表树形化阈值,即链表转成红黑树的阈值,在存储数据时,当链表长度 大于8 时,则将链表转换成红黑树。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树还原为链表的阈值,当在扩容时,resize()方法的split()方法中使用到该字段
 * 在重新计算红黑树的节点存储位置后,当拆分成的红黑树链表内节点数量 小于等于6 时,则将红黑树节点链表转换成普通节点链表。
 * <p>
 * 该字段仅仅在split()方法中使用到,在真正的remove删除节点的方法中时没有用到的,实际上在remove方法中,
 * 判断是否需要还原为普通链表的个数不是固定为6的,即有可能即使节点数量小于6个,也不会转换为链表,因此不能使用该变量!
 */
static final int UNTREEIFY_THRESHOLD = 6;


/**
 * 哈希表树形化的最小容量阈值,即当哈希表中的容量  大于等于64 时,才允许树形化链表,否则不进行树形化,而是扩容。
 */
static final int MIN_TREEIFY_CAPACITY = 64;


/**
 * 底层存储key-value数据的数组,长度必须是2的幂次方。由于HashMap使用"链地址法"解决哈希冲突,table中的一个节点是链表头节点或者红黑树的根节点。节点类型为Node类型,后面我们会分析到,Node的实际类型可能表示链表节点,也可能是红黑树节点。
 */
transient Node<K, V>[] table;


/**
 * 集合中键值对的数量,可通过size()方法获取。
 */
transient int size;


/**
 * 扩容阈值(容量 x 加载因子),当哈希表的大小大于等于扩容阈值时,哈希表就会扩容。
 */
int threshold;

/**
 * 哈希表实际的加载因子。
 */
final float loadFactor;

3 主要内部类

HashMap的内部类比较多,这里讲解主要的内部节点类,一个是Node节点,即普通链表节点;另一个是TreeNode节点类,即红黑树节点。

实际上Node节点直接实现了Map.Entry(Map体系中的集合的内部节点实现类的超级接口),实现了Map.Entry接口的全部方法。而TreeNode直接继承了LinkedHashMap.Entry节点类,而LinkedHashMap类中的节点类Entry则是继承了Node节点类。

虽然它们的关系有点绕,但是TreeNode和Node仍然属于Map.Entry节点体系,并且TreeNode节点类通过LinkedHashMap.Entry间接继承了Node节点类(爷孙关系)。因此底层数组table中的Node的实际类型可能就是链表节点Node,也可能是红黑树节点TreeNode。

3.1 Node

/**
 * JDK1.8的HashMap的链表节点实现类(JDK 1.7 使用Entry类,只是名字不一样)。
 * <p>
 * 具有hash属性,用于存放key的hashCode方法的返回值,避免重复计算。
 * 具有key、value属性用于存放键值对。
 * 具有next属性,由于HashMap使用"链地址法"解决哈希冲突,因此使用next指向后来加入的 存放在同一个桶位置(即哈希冲突)的节点。
 * <p>
 * 实现了Map.Entry(Map体系中的集合的内部节点实现类的超级接口)。
 * 实现了Map.Entry接口的全部方法,比如getKey、getValue、setValue,总之:比较简单。
 */
static class Node<K, V> implements Map.Entry<K, V> 
    final int hash;
    final K key;
    V value;
    Node<K, V> next;

    Node(int hash, K key, V value, Node<K, V> next) 
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    

    public final K getKey() 
        return key;
    

    public final V getValue() 
        return value;
    

    public final String toString() 
        return key + "=" + value;
    

    public final int hashCode() 
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    

    public final V setValue(V newValue) 
        V oldValue = value;
        value = newValue;
        return oldValue;
    

    public final boolean equals(Object o) 
        if (o == this)
            return true;
        if (o instanceof Map.Entry) 
            Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                return true;
        
        return false;
    

3.2 TreeNode

/**
 * JDK1.8的HashMap的红黑树节点实现类,直接实现了LinkedHashMap.Entry节点类。
 * <p>
 * 具有传统红黑树节点该有的属性,比如两个子节点、父节点、节点颜色等。
 * 具有链表树化的方法和树还原链表的方法,具有查找、存放、移除树节点的方法,具有调整平衡、左旋、右旋的方法,总之:比较复杂。
 * 后面会具体分析它的源码!
 */
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;

    /**
     * 构造方法,实际上只是调用了父类LinkedHashMap.Entry的构造方法,
     * 而在LinkedHashMap.Entry的对应构造方法中,又调用父类Node的构造方法
     *
     * @param hash key的hashCode返回值
     * @param key  k
     * @param val  v
     * @param next 链表的下一个节点引用
     */
    TreeNode(int hash, K key, V val, Node<K, V> next) 
        super(hash, key, val, next);
    

    /**
     * 返回包含此节点的树的根节点
     */
    final TreeNode<K, V> root() 
        //……
    

    /**
     * 把给定的节点是设置桶的第一个节点,即数组的节点
     */
    static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) 
        //……
    

    /**
     * 从当前节点开始通过给定的hash和key查找节点
     */
    final TreeNode<K, V> find(int h, Object k, Class<?> kc) 
        //……
    

    /**
     * 从根节点开始通过给定的hash和key查找节点
     */
    final TreeNode<K, V> getTreeNode(int h, Object k) 
        //……
    

    /**
     * 比较节点大小,用来排序
     */
    static int tieBreakOrder(Object a, Object b) 
        //……
    

    /**
     * 链表树化
     */
    final void treeify(Node<K, V>[] tab) 
        //……
    

    /**
     * 红黑树还原为链表,removeTreeNode、split方法中会调用到
     */
    final Node<K, V> untreeify(HashMap<K, V> map) 
        //……
    

    /**
     * 存放树节点
     */
    final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,
                                            int h, K k, V v) 
        //……
    

    /**
     * 删除树节点
     */
    final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
                              boolean movable) 
        //……
    


    /**
     * resize时,对红黑树节点的调用方法,包含了untreeify的逻辑
     */
    final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) 
        //……
    

    /**
     * 左旋
     */
    static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
                                                    TreeNode<K, V> p) 
        //……
    

    /**
     * 右旋
     */
    static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
                                                     TreeNode<K, V> p) 
        //……
    

    /**
     * 插入后调整平衡
     */
    static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
                                                          TreeNode<K, V> x) 
        //……
    

    /**
     * 删除后调整平衡
     */
    static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
                                                 TreeNode<K, V> x) 
        //……
    

    /**
     * 递归不变性检查
     */
    static <K,V> boolean checkInvariants(TreeNode<K,V> t) 
        //……
    

4 构造器

4.1 HashMap()

public HashMap()

构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

/**
 * 默认构造函数,可以看到并没有进行底层数组初始化,只是设置了加载因子为默认加载因子,即0.75
 */
public HashMap() 
    this.loadFactor = DEFAULT_LOAD_FACTOR;

4.2 HashMap(initialCapacity)

public HashMap(int initialCapacity)

构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。如果初始容量为负,则抛出IllegalArgumentException异常。

public HashMap(int initialCapacity) 
    //内部调用指定容量大小和默认加载因子0.75的构造函数
    this(initialCapacity, DEFAULT_LOAD_FACTOR);

4.3 HashMap(initialCapacity, loadFactor)

public HashMap(int initialCapacity,float loadFactor)

构造一个带指定初始容量和加载因子的空 HashMap。如果初始容量为负或者加载因子为非正,则抛出IllegalArgumentException异常。

注意,加载因子可以大于1。

/**
 * 构造一个带指定初始容量和加载因子的空 HashMap。
 * 如果初始容量为负或者加载因子为非正,则抛出IllegalArgumentException异常。
 */
public HashMap(int initialCapacity, float loadFactor) 
    /*1 参数检测*/
    // 如果指定的初始容量小于0,那么抛出IllegalArgumentException异常
    // 指定初始容量必须非负数,否则报错
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);

    // 如果指定的初始容量如果大于最大容量,那么初始容量等于最大容量
    // 即HashMap的最大容量只能是MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // 如果加载因子小于等于0或者不是一个数值类型,那么抛出IllegalArgumentException异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);

    /*2 初始化参数*/
    /*到这一步,说明上面的检测全部通过*/
    // 设置实际加载因子
    this.loadFactor = loadFactor;

    // 设置“扩容阈值”。注意这里设置的值并不是真正的阈值,因为我们说过容量只能是2的幂次方,因此需要先确保容量是2的幂次方才行
    // 这里的tableSizeFor方法仅仅只是将传入的容量转化为大于等于该容量的最小2的幂次方,然后赋值给threshold这个变量临时存储真正的初始容量,而真正的阈值在后面还会重新计算
    /*2.1 设置真正的初始容量*/
    this.threshold = tableSizeFor(initialCapacity);



/**
 * 2.1 tableSizeFor(initialCapacity),类似于JDK 1.7 中 inflateTable()里的 roundUpToPowerOf2(toSize),或者类似于JDK1.8 ArrayDeque中的allocateElements(numElements)方法
 * 该方法用于将传入的容量转化为大于等于该容量的最小2的幂次方值,即用于计算真正的初始化容量
 */
static final int tableSizeFor(int cap) 
    // 使用cap-1来计算,是因为下面的5行算法是 尝试查找大于数n的最小2的幂次方-1,因此要想查找大于等于cap的最小2的幂次方,只能使用cap-1来进行运算
    int n = cap - 1;
    //使用无符号右移和位或操作,尝试将n的最高位1的所有低位全部都变成1,即变成了一个奇数。
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //上面的算法看起来很完美,但是实际上还是有两个局限性的:
    //1 如果cap=0,那么经过上面的算法计算出来的n为-1,小于了0
    //2 如果cap> (1<<30) ,那么经过上面的算法计算出来的n为2147483647,即int最大值,大于了最大容量
    //因此还需要下面的3个判断:
    //1如果n小于0,即如果cap传入0,那么n=0-1=-1,那么返回初始容量1
    //2否则,如果n大于等于最大容量,那么就返回最大容量;
    //3否则,由于此时n为(大于原n的最小2的幂次方-1),n+1之后正好是大于等于cap的最小2的幂次方,返回n+1。
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

关于上面tableSizeFor的算法更详细解释,我在这篇文章的“ArrayDeque(int numElements)”部份有解释:Java集合—ArrayDeque的源码深度解析以及应用介绍。另外在JDK1.8的Integer类中的highestOneBit方法也使用了类似的算法,不过它是尝试返回小于等于参数的2的幂次方!

4.4 HashMap(m)

public HashMap(Map<? extends K,? extends V> m)

构造包含指定Map的新HashMap。所创建的HashMap具有默认加载因子(0.75)和足以容纳指定 Map 中键值对的初始容量。如果指定的映射为 null,则抛出NullPointerException异常。

/**
 * 构造包含指定Map的新HashMap。
 * 所创建的HashMap具有默认加载因子(0.75)和足以容纳指定 Map 中键值对的初始容量。
 */
public HashMap(Map<? extends K, ? extends V> m) 
    //设置加载因子为默认值0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //1 将传入的子Map中的全部元素逐个添加到HashMap中
    putMapEntries(m, false);



/**
 * 1 将传入的子Map中的全部元素逐个添加到HashMap中
 * @param m 被加入的集合
 * @param evict 在构造器中调用该方法时传入false,其他方法中(比如put、putAll)调用该方法时传入true。实际上在HashMap中没啥用,是留给其子类linkedHashMap用于实现LRU缓存的!
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 
    //s为m的实际元素个数
    int s = m.size();
    if (s > 0) 
        // 判断table是否已经初始化
        if (table == null)  // pre-size
            // 未初始化,计算初始容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 计算得到的t大于阈值,则初始化阈值
            if (t > threshold)
                threshold = tableSizeFor(t);
        
        // 已初始化,并且m元素个数大于阈值,进行扩容处理
        else if (s > threshold)
            resize();
        // 然后,将m中的所有元素循环添加至HashMap中
        for (Map.Entry<? extends K, Java 1.8 HashMap源码解析 桶数组+单链表+红黑树

HashMap源码解析

HashMap源码解析

jdk1.8源码解析:HashMap底层数据结构之链表转红黑树的具体时机

HashMap源码深入研究

HashMap源码解析