【java集合】HashMap源码解析(基于JDK1.8)

news2024/9/28 11:22:50

一、Hashmap简介

类继承关系图如下:
image.png
HashMap实现了三个接口,一个抽象类。主要的方法都在Map接口中,AbstractMap抽象类实现了Map方法中的公共方法,例如:size(),containsKey(),clear()等,主要方法由子类自己实现。

HashMap结构如下图:

image.png

HashMap的主要结构由数组、链表/红黑树组成,当数组中某个节点大于等于8个并且数组长度大于等于64时,链表会转换为红黑树。

二、HashMap的主要属性

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

    private static final long serialVersionUID = 362498820763181265L;
    
    /* ---------------- 默认值 -------------- */
    
    /**
     * 默认初始化大小,必须是二的次幂
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 数组最大值,当大于该值时使用该值
     */
    static final int MAXIMUM_CAPACITY = 1 << 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的数组(划重点,主要结构)
     * HashMap的由数组+链表/红黑树组成,数组指的就是这个数组,链表和红黑树则是由Node结构组成
     */
    transient Node<K,V>[] table;

    /**
     * 保存缓存的set.AbstractMap字段用于实现keySet()和values()。
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * HashMap中数据量大小.
     */
    transient int size;

    /**
     * HashMap的修改次数
     */
    transient int modCount;

    /**
     * 阈值,当HashMap中数据大于该值时将进行扩容
     */
    int threshold;

    /**
     * 加载因子
     */
    final float loadFactor;
    
    
    /* ---------------- Node结构 -------------- */
   
    /**
     * HashMap的基础节点
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        //当前节点的hash值
        final int hash;
        //当前节点的key
        final K key;
        //当前节点的value
        V value;
        //指向下一个节点的引用
        Node<K,V> next;

        //node的构造函数
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //...其他方法省略
    }

    

三、源码解析

1、初始化方法

HashMap共有四个构造函数,参数分别是1、初始化数组大小、加载因子。2、初始化数组大小。3、无参。4、HashMap结构。

其中1、2、3三个参数的构造函数性质相同,都是传入初始化数组大小或加载因子,没传的使用默认值。构造函数4使用默认的初始化大小和加载因子,并且是将传入的HashMap添加到新的结构中。

具体代码如下:

/**
 * 有初始化大小和加载因子大小的构造函数
 * @param  initialCapacity 初始化大小
 * @param  loadFactor      加载因子
 * @throws IllegalArgumentException 非法参数异常
 */
public HashMap(int initialCapacity, float loadFactor) {
    //如果初始化大小小于0,抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果初始化大小大于最大值,那么就把初始化大小设置为最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //如果加载因子小于等于0或者是非法的float类型,则抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //设置加载因子
    this.loadFactor = loadFactor;
    //设置下一次扩容阈值
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 有初始化大小的构造函数 加载因子使用默认值(0.75)
 *
 * @param  初始化大小
 * @throws IllegalArgumentException 非法参数异常
 */
public HashMap(int initialCapacity) {
    //调用第一个构造函数,加载因子使用默认值DEFAULT_LOAD_FACTOR(0.75)
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 无参构造函数,使用默认大小(16)和默认加载因子(0.75)
 */
public HashMap() {
    //设计加载因子为默认值,其他所有值都是要默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 使用一个HashMap为参数的构造函数,加载因子使用默认(0.75)
 *
 * @param   一个HashMap数据
 * @throws  NullPointerException 空指针异常
 */
public HashMap(Map<? extends K, ? extends V> m) {
    //加载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //将传入的HashMap数据放入当前结构中
    putMapEntries(m, false);
}

2、get方法

先看源码,再做总结,源码:

/**
 * HashMap的get方法
 * @param   要查找的key
 */
public V get(Object key) {
    //定义一个node
    Node<K,V> e;
    //通过getNode()方法获取node,getNode返回null则get方法返回null,否则返回node的value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * 实现Map.get和相关方法
 * @param key的hash值
 * @param key
 * @return 结构中的node或者null
 */
final Node<K,V> getNode(int hash, Object key) {
    //定义说明  tab:数组,first:该数组节点中的第一个值,n:数组大小,k:first的key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //如果数组不为null、数组大小大于0、通过hash获取到的数组中的节点不为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //该节点的hash等于要查找的hash值(始终检查第一个节点)、该节点的key与要查找的key相等(==为true或者equals为true)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果第first节点不为null并且first节点不是要查找的节点(上面的if判断,如果是要查找的接口则上一步就返回了)
        if ((e = first.next) != null) {
            //如果是红黑树类型
            if (first instanceof TreeNode)
                //遍历红黑树
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //循环遍历链表
            do {
                //当hash值相同、该节点的key与要查找的key相等(==为true或者equals为true)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //如果hash值对应的数组为null,则返回null
    return null;
}

get方法总结:

  1. 根据key的hash值找到数组中对应的位置。
  2. 判断该位置上的值和要查找的值是否相等(==或者equals),如果是则返回
  3. 如果不是则判断该节点的下一个节点是否为空,为空则返回null。
  4. 判断结构是否是红黑树,如果是,遍历树。
  5. 如果不是树,则遍历链表。
  6. 如果不符合上面的条件则返回null。

3、put方法

先看源码:

/**
 * 添加key-value,如果key已经对应value,则替换,返回之前的值
 *
 * @param key
 * @param value
 * @return 返回之前的value
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 实现Map.put和相关方法
 *
 * @param hash值
 * @param key
 * @param value
 * @param onlyIfAbsent 是否只在不存在的时候修改值,true不修改,false修改
 * @param evict 如果为false,则为创建模式
 * @return 返回之前的value,如果没有则为null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //变量说明tab:当前数组,p:当前节点,n:数组大小,i:要插入的数据在数组中的位置
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //数组为空或者数组大小为0 初始化数组(resize()扩容函数,也包括初始化数组,后面扩容会分析)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果对应数组中的位置为null,将当前数据构造成Node放入该节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //当hash值相同、该节点的key与要插入的key相等(==为true或者equals为true),则替换该value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果是树结构,插入树节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //遍历链表节点
            for (int binCount = 0; ; ++binCount) {
                //如果没有遍历到与该key相同的数据,则在链表最后添加该数据节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果该链表长度大于等于8,则将链表转换为树
                    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 = e;
            }
        }
        //如果存在key对应的数据,替换数据,返回之前的数据
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //LinkedHashMap使用,HashMap中方法体为空
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果数组中的值大于阈值,则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

put方法总结:

  1. 判断HashMap中的数组是否为空或者大小为0,如果是则初始化数组。
  2. 如果该hash值对应的数组中的位置为空,则将该数据组成的节点直接插入到该位置中。
  3. 如果数组对应位置数据不为空,判断该位置节点的key与要插入的key是否相等,如果是设置e(局部变量)等于该节点。
  4. 如果Node结构为树结构,则遍历树结构找到key对应的节点,设置为e。
  5. 如果Node结构为链表,遍历链表,如果链表中没有找到对应的key,将数据构造成Node节点插入链表最后,如果链表长度大于等于8,则将结构转为树。
  6. 如果在链表中找到对应的key,则将该节点设置为e.
  7. 如果e(上面遍历找到的节点)不为null,则设置新的value,返回旧的value.
  8. 如果e为null,说明是新增,HashMap大小加一,判断是否大于阈值,如果大于,则扩容。

4、扩容

先看源码:

/**
 * 初始化或者扩容
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    //旧的数组
    Node<K,V>[] oldTab = table;
    //旧的数组大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧的阈值
    int oldThr = threshold;
    //新的数组大小和阈值
    int newCap, newThr = 0;
    //如果旧的数组大小大于0(只要初始化过就都会大于0)
    if (oldCap > 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; // double threshold
    }
    //如果数组大小为0,阈值大小大于0,则设置新的初始化大小为阈值,否则全部使用默认值
    else if (oldThr > 0) // 初始容量设置为阈值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新的阈值等于0,那么设置新的阈值为新的数组大小*加载因子
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //阈值等于新的阈值
    threshold = newThr;
    //新的数组
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //旧的数组不为null,说明不是初始化,需要扩容
    if (oldTab != null) {
        //遍历旧得数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果该位置的节点不为null,那么遍历链表或者树放入新的数组中
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果只有一个节点,直接放入新数组中对应的位置
                if (e.next == null)
                    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;
                        //如果hash值&旧的数组大小为0,说明放到新数组后还是之前的位置,否则为(当前位置+旧数组大小)的位置
                        //遍历第一个节点时,头、尾都设置为该节点,之后的节点添加到该节点之后,并设置尾节点为后添加的节点
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //设置新数组的节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容方法总结:

  1. 数组是否已经达到最大值,如果已经最大,设置阈值也为最大值,否则数组大小和阈值都改为之前的两倍。
  2. 数组是否已经初始化,如果没有则初始化数组和阈值。
  3. 旧数组不为null,遍历旧数组,将对应位置的链表/树分为成两个链表/数组,一个在原先的位置上,一个在原先的位置+原先数组大小的位置上,将两个链表/树放入新数组的对应位置。

【java集合】HashMap源码解析(基于JDK1.8)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/139570.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux驱动之系统移植----uboot移植_修改网络驱动(uboot无设备树版本)

uboot版本:uboot.2016.03 开发板:100ask_imx6ull_pro 修改网络驱动 须知 I.MX6UL/ULL内部有个以太网 MAC外设&#xff0c;也就是 ENET&#xff0c;需要外接一个 PHY芯片来实现网络通信功能&#xff0c;也就是内部 MAC外部 PHY芯片的方案。(一个MAC可对应N个PHY芯片, PHY有地址…

5G NR标准 第11章 多天线传输

第11章 多天线传输 多天线传输是 NR 的关键组成部分&#xff0c;尤其是在较高频率下。 本章一般性地介绍了多天线传输的背景&#xff0c;然后详细描述了 NR 多天线预编码。 11.1 简介 使用多个天线进行传输和/或接收可以在移动通信系统中提供巨大的好处。 发射机和/或接收…

女友:不跟我解释清楚 Linux I/O 晚上你就睡沙发吧

目录阻塞 I/O非阻塞 I/OI/O复用信号驱动 I/O异步 I/O总结I/O 其实就是 input 和 output&#xff08;输入输出&#xff09; 在计算机操作系统中对应数据流的输入与输出&#xff0c;在 Linux 中&#xff0c;既有文件的 I/O&#xff0c;也有网络 I/O 无论是文件 I/O 还是网络 I/…

【大厂高频真题100题】《整数转罗马数字》 真题练习第21题 持续更新~

整数转罗马数字 罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如, 罗马数字 2 写做 II ,即为两个并列…

【解决方案】智慧城管非现场执法系统

通过自建摄像头或利用辖区现有监控摄像头&#xff0c;利用人工智能技术&#xff0c;通过深度学习算法&#xff0c;系统能够全天候自动识别和采集城管违章行为&#xff0c;实现店外经营智能分析、无证游商智能分析、乱堆物堆料智能分析、暴露垃圾等场景的智能分析&#xff0c;从…

黄金分析师职级如何评定?初级、高级如何划分?

自从中国加入WTO以后&#xff0c;对外贸易和境外投资持续高速增长&#xff0c;为国家带来了了巨额外汇积累。发展黄金市场、提高黄金投资需求&#xff0c;成为我国应对国际金融风险显现和美元贬值的重要举措。然而&#xff0c;黄金投资市场健康、持续的发展&#xff0c;需要大量…

shell的模拟实现

目录 整体框架分析 代码演示 代码分析 整体框架分析 考虑下面这个与shell典型的互动&#xff1a; [xzyecs-333953 date16]$ ls makefile mycmd mycmd.cpp myexec myexec.c test.py [xzyecs-333953 date16]$ psPID TTY TIME CMD 21919 pts/0 00:00:00 bash …

jmeter之数据库配置及修改(工作日记3)

首先&#xff0c;jmeter要配置数据库需要安装 jdbc.jar包 官网连接&#xff1a;https://dev.mysql.com/downloads/ 步骤1&#xff1a; 步骤二&#xff1a; 步骤三&#xff1a; 步骤四&#xff1a; 接着我们去jmeter导入此jar包&#xff1a; 然后我们就可以在jmeter中去…

CSS3 多列布局

文章目录CSS3 多列布局概述常用属性使用column-count 列数column-width 列宽column-gap 间距column-rule 边框column-span 跨列案例瀑布流布局CSS3 多列布局 概述 在CSS3之前&#xff0c;如果想要设计类似报纸那样的多列布局&#xff0c;有两种方式可以实现&#xff1a;一种是…

kaggle平台学习复习笔记 | pandas

目录1.Creating, Reading and Writing2.Indexing, Selecting & Assigning3.Summary Functions and Maps4.Grouping and Sorting5.Data Types and Missing Values6.Renaming and Combining获得开展独立数据科学项目所需的技能。 1.Creating, Reading and Writing DataFram…

安全帽识别算法技术原理

应用背景&#xff1a;安全帽作为一种最常见和实用的个人防护用具&#xff0c;能够有效地防止和减轻外来危险源对头部的伤害。但在现场操作过程中&#xff0c;安全帽的佩戴很容易人为忽略&#xff0c;引发了不少人身伤害事故。为了保证工作人员都能在作业中佩戴安全帽&#xff0…

CentOS/Red Hat 安装cuDNN

创建于&#xff1a;2023.01.04 修改于&#xff1a;2023.01.04 文章目录1、找到cuDNN版本号2、下载cuDNN3、安装cuDNN4、参考资料本文是在Linux Red Hat下安装的cuDNN&#xff0c;且假定您已经安装好了NVIDIA驱动、CUDA。 本文是在CUDA 11.2 Red Hat条件下安装的cuDNN。 1、找…

超高清直播“出圈”下的沉浸式文博会

在引领文化产业发展的文博会现场&#xff0c;前沿科技元素随处可见。VR、AR、4K/8K、元宇宙……各种“文化科技”的元素以刷新视听极限的美好体验&#xff0c;冲击着观众的眼球和感知。文博会现场&#xff0c;BOSMA博冠于数字文化展区展示国产8K超高清摄像机、BOSMA博冠望远镜等…

(小甲鱼python)集合笔记合集一 集合(上)总结 集合的简单用法 集合的各种方法合集:子、交、并、补、差、对称差集、超集

一、基础复习 集合与字典区别 集合中所有元素都是独一无二的&#xff0c;并且也是无序的。 集合具有唯一性、无序性、有限性 >>> type({}) #字典 <class dict> >>> type({"one"}) #集合 <class set> >>> type…

微服务保护 Sentinel 实战

目录Sentinel什么是雪崩问题&#xff1f;服务保护技术对比Sentinel介绍和安装初识Sentinel安装Sentinel微服务整合Sentinel流量控制簇点链路快速入门流控模式关联模式链路模式流控效果warm up排队等待热点参数限流全局参数限流热点参数限流案例隔离和降级FeignClient整合Sentin…

nVisual各项隐藏项配置说明

nVisual可以部署在不同的项目中&#xff0c;但是每个项目都有自己不同的使用场景&#xff0c;有的不需要报表功能&#xff0c;有的不需要监测功能&#xff0c;还有的不需要连接外网的功能等。针对这些不同的需求&#xff0c;有必要且有可使用的机制来满足这些场景。 nVisual使…

PageRank算法

什么是PageRank&#xff1f; PageRankPageRankPageRank算法由GoogleGoogleGoogle创始人LarryPageLarry\ PageLarry Page在斯坦福大学时提出&#xff0c;又称PRPRPR&#xff0c;佩奇排名。主要针对网页进行排名&#xff0c;计算网站的重要性优化搜索引擎的搜索结果。PRPRPR值表示…

Java大学生档案系统高校学生档案系统

简介 Java基于springboot开发的学生档案系统&#xff0c;包含了权限管理&#xff0c;学生档案&#xff0c;学生成绩录入&#xff0c;教师管理等。 演示视频 https://www.bilibili.com/video/BV1XV411b761/?share_sourcecopy_web&vd_sourceed0f04fbb713154db5cc611225d92…

SQL速算N日留存

之前才哥发布了《用SQL进行用户留存率计算》 链接&#xff1a;https://mp.weixin.qq.com/s/QJ8JUO00bVJe_K6sx_ttaw 简化数据后得到如下结构的数据&#xff1a; 由于用户和登录日期被设置为主键所以不需要再进行去重&#xff0c;下面看看如何快速求七日留存。 数据下载地址&…

数据中心基础设施管理(DCIM)工具

什么是DCIM DCIM (Data Center Infrastructure management) 数据中心基础设施管理 是将IT&#xff08;信息技术&#xff09;和设备管理结合起来对数据中心关键设备进行集中监控、容量规划等集中管理。通过软件、硬件和传感器等&#xff0c;DCIM提供一个独立的管理平台&#xf…