全网最硬核的源码分析之——HashMap源码分析上

Posted 程序员梦尘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全网最硬核的源码分析之——HashMap源码分析上相关的知识,希望对你有一定的参考价值。

HashMap源码分析

创造不易,希望大家可关注一下,点个赞再走,感谢您的阅读。

基于JDK1.8

一  数据结构


HashMap 底层的数据结构主要是:数组 + 链表 + 红黑树。其中当链表的长度大于等于 8 时(且数组的长度达到一定的长度)后,链表会转化成红黑 树,当红黑树的大小小于等于 6 时,红黑树会转化成链表

1.1 重要变量

//初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
transient int modCount;
//HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种情况
// 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小
为 32,为 2 的 5 次方。
// 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
//链表的节点
static class Node<K,Vimplements Map.Entry<K,V{
//红黑树的节点
static final class TreeNode<K,Vextends LinkedHashMap.Entry<K,V

注意事项

(1)容量

 容量为数组的长度,亦即桶的个数,默认为16,最大为2的30次方,当容量达到64时才可以树化。

(2)装载因子

 装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。

(3)树化

  树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。 

二.源码分析

2.1 HashMap类注释解析

  • 允许 null 值,不同于 HashTable ,是线程不安全的;
  • load factor(影响因子) 默认值是 0.75, 是均衡了时间和空间损耗算出来的值,较高的值会减少空间开销(扩 容减少,数组大小增长速度变慢),但增加了查找成本(hash 冲突增加,链表长度变长),不扩容的条件:数 组容量 > 需要的数组大小 /load factor;
  • 如果大量数据需要储存到 HashMap 中,建议 HashMap 的容量一开始就设置成足够的大小,这样可以防止在 其过程中不断的扩容,影响性能;
  • HashMap 是非线程安全的,我们可以自己在外部加锁,或者通过 Collections#synchronizedMap 来实现线程安 全,Collections#synchronizedMap 的实现是在每个方法上加上了 synchronized 锁;
  • 在迭代过程中,如果 HashMap 的结构被修改,会快速失败。

2.2 内部类

2.2.1 Node

Node是一个单链表节点,其中,hash用来存储key计算得来的hash值。

static class Node<K,Vimplements Map.Entry<K,V{
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

2.2.2 TreeNode内部类

继承自LinkedHashMap中的Entry类

TreeNode是一个树型节点,其中,prev是链表中的节点,用于在删除元素的时候可以快速找到它的前置节点。

// 位于HashMap中
static final class TreeNode<K,Vextends LinkedHashMap.Entry<K,V{
    TreeNode<K,V> parent;  // 红黑树节点
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // 用于在删除元素的时候可以快速找到它的前置节点。
    boolean red;
}

// 位于LinkedHashMap中,双向链表节点
static class Entry<K,Vextends HashMap.Node<K,V{
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

2.3 HashMap()构造方法

2.3.1 空参构造方法,全部使用默认值。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

2.3.2 HashMap(int initialCapacity)构造方法

调用HashMap(int initialCapacity, float loadFactor)构造方法,传入默认装载因子。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

2.3.3 HashMap(int initialCapacity, float loadFactor)构造方法

判断传入的初始容量和装载因子是否合法,并计算扩容门槛,扩容门槛为传入的初始容量往上取最近的2的n次方。

public HashMap(int initialCapacity, float loadFactor) {
    // 检查传入的初始容量是否合法
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 检查装载因子是否合法
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // 计算扩容门槛
    this.threshold = tableSizeFor(initialCapacity);
}

static final int tableSizeFor(int cap) {
    // 扩容门槛为传入的初始容量往上取最近的2的n次方
    int n = cap - 1;
    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;
}

推荐阅读

如果感觉对您有帮助,希望大家可关注一下,点个赞再走,感谢您的阅读。后续会有更多原创文章


以上是关于全网最硬核的源码分析之——HashMap源码分析上的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 2从入门到入坟 | 请求参数处理篇:全网最硬核的请求映射原理的源码分析

HashMap详细解释+全站最硬核手撕源码分析

HashMap详细解释+全站最硬核手撕源码分析

全网最硬核的 synchronized 面试题深度解析

全网最硬核的 synchronized 面试题深度解析

全网最硬核的 synchronized 面试题深度解析