前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HashMap深度解析

HashMap深度解析

作者头像
全栈开发日记
发布2022-05-13 14:42:10
1920
发布2022-05-13 14:42:10
举报
文章被收录于专栏:全栈开发日记

本篇目录:

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扩容机制解析”另写一篇。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈开发日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HashMap底层数据结构
  • HashMap中的常量
  • HashMap中的变量
  • HashMap的初始化
  • 初始化后第一次put
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档