HashMap深度解析

2022-05-13 14:42:10 浏览数 (1)

本篇目录:

HashMap底层数据结构 HashMap中的常量 HashMap中的变量 HashMap的初始化 初始化后第一次调用put

HashMap底层数据结构

JDK1.8之前HashMap由数组 链表组成。数组是HashMap的主体,链表主要是为了解决哈希冲突而存在。哈希冲突是两个对象调用hashCode方法计算的哈希值相同导致计算的数组索引值相同。

JDK1.8之后HashMap由数组 链表 红黑树组成,当链表长度大于8,且数组长度大于64时,该索引位置上的所有数据改使用红黑树存储。当链表长度大于8,但数组长度不大于64时,链表也不会转换成红黑树,而是会扩容数组。

详见源码:

代码语言:javascript复制
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 判断数组长度是否小于最小数组长度阈值
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

图片来自网络

HashMap中的常量

代码语言:javascript复制
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认初始大小
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子(负载因子)
static final int TREEIFY_THRESHOLD = 8; //当前链表长度转红黑树的阈值
static final int UNTREEIFY_THRESHOLD = 6; //红黑树转链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; //链表转红黑树的数组长度阈值

HashMap中的变量

代码语言:javascript复制
/* EntrySet实体,定义成变量可以保证不被重复创建,是一个Map.Entry集合
* Map.Entry是一个接口,HashMap的内部类Node就实现了该接口
*/
transient Set<Map.Entry<K,V>> entrySet; 

/* 用来存储HashMap中的键值对,是一个Node数组
* Node是一个内部类,实现了Map.Entry接口,就是一个单向链表
*/
transient Node<K,V>[] table;

/* 记录HashMap中键值对的数量,put和remove都会对其造成改变*/
transient int size;

/* 当HashMap内部结构发生变化时才会修改
* 当HashMap内部结构发生改变时时则会  modCount
*/
transient int modCount;

/* threshold表示一个临界值,当HashMap的size大于这个值的时候进行resize */
int threshold;

HashMap的初始化

HashMap的四个构造方法:

代码语言:javascript复制
// 指定初始大小和加载因子
public HashMap(int initialCapacity, float loadFactor) {...}

// 指定初始大小
public HashMap(int initialCapacity) {...}

// 无参构造,全部使用默认常量
public HashMap() {...}

// 使用现有的Map来构造HashMap
public HashMap(Map<? extends K, ? extends V> m) {...}

与ArrayList不同,new ArrayList()时,ArrayList的底层数组大小是0,在添加第一个元素后大小会被扩容为10。

HashMap在JDK1.8之前,在构造方法中就会初始化一个长度16的Entry[]数组用来存储键值对。

在JDK1.8以后,在第一次调用put方法时创建一个Node[]数组用来存储键值对数据,Node实现了Map.Entry接口。

Node源码:该类是一个内部类

代码语言:javascript复制
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) {...}

    public final K getKey()        {...}
    public final V getValue()      {...}
    public final String toString() {...}

    public final int hashCode() {...}

    public final V setValue(V newValue) {...}

    public final boolean equals(Object o) {...}
}

初始化后第一次put

这里是代码执行流程:

put()方法:

代码语言:javascript复制
// 调用put方法
public V put(K key, V value) {
    // 这里调用了putVal方法
    return putVal(hash(key), key, value, false, true);
}

② 进入putVal()方法:

代码语言:javascript复制
// 判断当前表中是否为空(是不是第一次添加元素)
if ((tab = table) == null || (n = tab.length) == 0)
    // 进入resize方法
    n = (tab = resize()).length;

这里的变量table是HashMap中用来存储键值对的Node<K,V>[]数组。

如下:transient Node<K,V>[] table;

③ 进入resize()方法:

由于该方法内容比较多,所以这里以程序运行流程列明,其余代码请自行查看HashMap源码。

代码语言:javascript复制
// 以下内容以程序运行流程
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;

newCap = DEFAULT_INITIAL_CAPACITY; //16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //16 * 0.75 
threshold = newThr; //将新的临界值赋值给threshold
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建一个16长度的Node数组
table = newTab; //将长度16的数组赋值给变量table
return newTab; //将Node数组返回

④ 回到putVal方法

代码语言:javascript复制
if ((tab = table) == null || (n = tab.length) == 0)
    // 刚刚进入如下方法,得到n为初始化的数组长度16
    n = (tab = resize()).length;
// 通过&位运算 得到i=1,判断Node数组索引1的元素是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
    // 进入newNode方法,该方法会根据参数创建一个单向链表
    tab[i] = newNode(hash, key, value, null);
//这里省略不执行的代码
...
// 由于进行了初始化,所以HashMap内部结构发生了变化,所以  modCount
  modCount;
// 这里判断了HashMap的键值对数量是否大于扩容阈值
if (  size > threshold)
    resize();
// 该方法是为了继承HashMap的LinkedHashMap类服务,这里没有实际作用
afterNodeInsertion(evict);
// 到这里第一次调用put就结束了
return null;

以上内容调用的newNode方法:

代码语言:javascript复制
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    // 返回Node链表
    return new Node<>(hash, key, value, next);
}

由于篇幅已经很长了,所以“HashMap扩容机制解析”另写一篇。

0 人点赞