最全面的HashMap解读

下图是JDK 1.8中HashMap在Idea中查看的整体结构,关于最重要的部分,我在图中添加了文字说明。下面将从默认静态常量,成员变量,构造函数,核心方法,迭代遍历,与JDK1.7中的不同等6个方面进行学习。
对HashMap源码最详细的注释我都写到这里面了,可以点击链接下载

1 六个静态常量

能搜到这篇文章,说明你对散列,链表,HashMap等知识有一定了解了。

// 默认初始容量 - 必须是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默认最大容量 - 如果具有参数的任一构造函数隐式指定更高的值,则使用此方法。必须是2的幂 <= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值:一个链表大于这个值就要转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 反树化阈值:一个红黑树小于这个值就要转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * 最小树形化容量。当哈希表中的容量大于这个值时,表中的桶才能进行树形化
 * 否则桶内元素太多时会扩容,而不是树形化,为了避免进行扩容、树形化选择
 * 的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
 */
static final int MIN_TREEIFY_CAPACITY = 64;

这里可能比较绕的是三个树化相关的值理解,什么时候开始树化呢?答:哈希表容量必须要比MIN_TREEIFY_CAPACITY大,否则即使某个链表值大于TREEIFY_THRESHOLD也不会树化,而是进行扩容。
为什么capacity都要要求是二的整数幂?答:为了计算简便和提高效率啊,因为2的幂次用二进制表示非常方便高效的计算,后面计算数组索引位置取模运算使用的hash&(n-1)就是例子,看了后面的代码会更好理解。

2 六个成员变量

直接先看代码注释就理解的差不多了

// HashMap的桶位,是一个Node节点数组
transient Node<K,V>[] table;
// 缓存保存根据HashMap获取的Set集合
transient Set<Map.Entry<K,V>> entrySet;
// 当前存储的键值对个数
transient int size;
/*
 修改次数,因为HashMap是线程不安全,如果在迭代的过程中HashMap被其他线程修改了,
 modCount的数值就会发生变化, 这个时候expectedModCount和ModCount不相等, 迭
 代器就会抛出ConcurrentModificationException()异
 */
transient int modCount;
/*
    自动扩容阈值(threshold = capacity*loadFactor), capacity是当前桶位数,
    loadFactor是加载因子,默认0.75f;也就是当HashMap数据条数达到桶位数的75%时,
    就会自动扩容
*/
int threshold;
//加载因子(默认0.75f), size/capacity >= 0.75 = threshold/capacity 时将会扩容
final float loadFactor;

实际上不止六个成员变量,因为我们还从AbstractMap中继承了keySet, values,所以总共加起来有8个左右。
table 是个数组,该数组的每一个位置存放的可能都是一个链表或者一颗红黑树的首(根)节点,也就是HashMap真正存键值对的容器。
modeCount 主要是防止在遍历等操作是修改HashMap的结构,在具有修改HashMap结构的方法(removeNode,putVal等)中该计数器都会在修改成功后增加。
threshold,capacity,loadFactor,size他们之间的关系在代码注释中已经说得很详细了。

3 构造函数

public HashMap(int initialCapacity, float loadFactor)...
public HashMap(int initialCapacity)...
public HashMap()...
public HashMap(Map<? extends K, ? extends V> m)...

总共有上面4个构造方法,详细源码就略了。
前3个构造函数 一看就知道啥意思了,无非自己指定容量值和加载因子。这里有一点需要注意,在2个参数的构造方法中,初始化的是threshold和loadFactor,所以我估计这里实际扩容是在添加时候判断阈值和capacity时才会真正扩容。
第4个构造函数 该构造函数调用putMapEntries往里面添加另外一个Map的所有数据。

4 核心方法

什么是核心方法,当然实现HashMap最基础操作的增加,查找,扩容,删除这几个方法。在上面的图中用加粗红色字体注释了,其他的一些方法是调用这几个主要方法实现自身功能的,图中用 @符号+黄色字体 表示。看懂这几个核心方法,@符号标注的那些方法就非常容易理解。

4.1 增加

看源码发现外部调用实际是调用的put,而实际代码逻辑在putVal方法中。

public V put(K key, V value) {
  // 调用putVal方法
  return putVal(hash(key), key, value, false, true);
}

/**
 * @Description: HashMap插入一个值的方法
 * @param hash  key的散列值
 * @param key   键对象
 * @param value 要保存的值对象
 * @param onlyIfAbsent 如果为true,不要替换存在的值对象
 * @param evict 如果为false,则表处于创建模式
 * @return 返回旧值,如果不存在返回null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; // HashMap的桶位数组,存放所有的键值对
    Node<K,V> p;     // 表示定位到的桶位tab[i]
    int n, i;        // n表示tab数组大小,i表示定位到的桶位索引

    // 1. 判断是否要resize
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 定位到要放的桶位,并进行修改
    // 2.1 tab[i] == null: 还没放过任何值,就直接放在该处
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
	// 2.2 tab[i] != null: 遍历该处的链表或树,找到要放的位置      
    else { 
        Node<K,V> e; K k;
        // 2.2.1 和tab[i]的key相匹配了
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 2.2.2 首节点tab[i]是一个TreeNode
        else if (p instanceof TreeNode) // 是树的根节点,调用putTreeVal插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 2.2.3 首节点是链表的节点
        else {
            for (int binCount = 0; ; ++binCount) {
                // 尾插法:找到最末端,插入一个节点,放入key和value
                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映射已经存在,直接跳出循环
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // p 指向下一个非空节点
                p = e;
            }
        }

        // 2.2.4 这里才是真正放值的时候,如果key重复, 根据onlyIfAbsent判断是否需要覆盖旧值
        if (e != null) {
            V oldValue = e.value; // 该节点旧值
            // 根据onlyIfAbsent判断是否要覆盖旧的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 回调方法
            return oldValue; // 返回旧值,如果返回旧值就不会执行下面逻辑,modCount也不会加1
        }

    }
    // 3 调整
    // 修改次数加1
    ++modCount;
    // size加一后,看要扩容不
    if (++size > threshold)
        resize();

    afterNodeInsertion(evict); // 回调方法
    return null;
}

该方法重要的是插入方法:使用了尾插法。而JDK1.7使用的是头插法。这点将在JDK1.7和1.8比较中讲解。

4.2 查找

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; // tab临时存放table
    /*first是key所在桶位的第一个节点,first = tab[(n - 1) & hash]
    * */
    Node<K,V> first, e;
    int n; // n表示table键值对数量
    K k; // 存放节点key的临时变量
    /*只有在table非空,且根据hash值得到对应桶位非空时,才开始查找对应桶位上的链表或红黑树*/
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 该桶位第一个节点满足“hash相同&&(key地址相等,或者key内容相同)”,就直接返回first
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 接着搜索first下面的节点
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) // 如果是红黑树,就调用getTreeNode搜索该树
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { // 依次判断链表的每一个节点,直到找到return或者遍历完该链表
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

4.3 扩容

/*扩容方法*/
final Node<K,V>[] resize() {
    // 1. 初始化必要的临时变量
    Node<K,V>[] oldTab = table; // 暂存旧容器
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量
    int oldThr = threshold; // 旧阈值
    int newCap, newThr = 0; // 初始化新的容量和阈值

    // 2. 计算新阈值和新容量
    if (oldCap > 0) { // 如果旧容量大于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; // 新容量没超过最大容量,阈值加倍

    }else if (oldThr > 0)
        newCap = oldThr; // 用旧阈值代替新容量
    else {               // 否则用默认值初始化新容量和新阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    if (newThr == 0) { // 上面分支计算完,如果新阈值为0
        float ft = (float)newCap * loadFactor; // 计算阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }

    threshold = newThr; // 得到最终阈值更新

    @SuppressWarnings({"rawtypes","unchecked"})
    // 3. 再散列过程
    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) { // 该桶位赋值给e,如果非空证明有元素需要分配
                oldTab[j] = null; // 置空旧桶的该位置
                if (e.next == null) //e.next为空,该桶位只有一个节点
                    // 直接存给新桶,这里通过e.hash & (newCap - 1)定位到新桶的位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) // 如果是树结构的根节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // ???
                else {
                    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; // loTail指向尾结点
                        }
                        else { // 索引值改变
                            if (hiTail == null)
                                hiHead = e; // 头结点
                            else
                                hiTail.next = e; // 连接新元素
                            hiTail = e; // hiTail指向尾结点
                        }
                    } while ((e = next) != null);

                    // 保存到(j)位置,即原位置
                    if (loTail != null) {
                        loTail.next = null; // 尾结点下一个节点设为null
                        newTab[j] = loHead; // 新容器数组前半段
                    }
                    // 保存到(oldCap + j)位置,即后半段
                    if (hiTail != null) {
                        hiTail.next = null; // 尾结点下一个节点设为null
                        newTab[j + oldCap] = hiHead; // 新容器数组后半段
                    }
                }
            }
        }
    }
    return newTab;
}

扩容的时候要注意是怎么计算

4.4 删除

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    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) {
        Node<K,V> node = null, e; K k; V v;
        // 1. 查找要删除的节点位置
        // 1.1 要删除的直接是桶位首节点
        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)
                // 1.2 在红黑树中搜索
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 1.3 在链表中搜索
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e; // 找到要删除的节点,赋给node
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 2.2 删除节点
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            // 如果是树,调用removeTreeNode删除该节点
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 如果是桶位第一个节点,将下一个节点直接放到该桶位
            else if (node == p)
                tab[index] = node.next;
            // 如果是链表中间某个节点node, 则前一个节点p直接指向该节点下一个节点
            else
                p.next = node.next;

            ++modCount; // 调整HashMap修改次数
            --size; // 调整键值对计数器
            afterNodeRemoval(node); // 回调

            return node; // 删除成功,返回被删除的节点
        }
    }
    return null; // 没有删除返回null
}

5 遍历

5.1 entrySet,keySet,values遍历的底层实现

HashMap底层是如何实现遍历的呢? 可以看看我的这篇文章详解Java中散列和HashMap中的第4.3.4节,分析HashMap是怎样实现遍历的,其实keySet,entrySet,values的遍历底层原理是非常相似的。
集合类的常用遍历方法总结 可以看看我的这篇文章Java集合类详解中的第8节

5.2 JDK新增的Spliterator遍历

6 JDK8与JDK7的对比

推荐这篇文章,等我整理的更好在详细总结这部分。
美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值