Java [ 基础 ] HashMap详解 ✨

news2024/11/17 8:26:38

目录

✨探索Java基础   HashMap详解✨

总述

主体

1. HashMap的基本概念

2. HashMap的工作原理

3. HashMap的常用操作

4. HashMap的优缺点

总结

常见面试题

常见面试题解答

1. HashMap的底层实现原理是什么?

2. 如何解决HashMap中的哈希冲突?

3. HashMap和Hashtable的区别是什么?

4. 在什么情况下HashMap会发生扩容?

5. 为什么HashMap不是线程安全的?如何实现线程安全的HashMap?

HashMap源码

1 put方法流程

2 扩容

3 get方法


✨探索Java基础   HashMap详解✨

总述

在Java中,HashMap 是一个常用的数据结构,它实现了Map接口,允许我们通过键值对的形式存储和快速查找数据。HashMap的底层是基于哈希表(hash table)的实现,它的高效性和灵活性使其在各种编程场景中广受欢迎。本文将详细介绍HashMap的原理、使用方法、优缺点,并提供一些常见的面试题。

主体
1. HashMap的基本概念

HashMap是一个散列表,它存储键值对(key-value pairs),每个键对应一个唯一的值。HashMap不保证顺序,并且允许null值作为键或值。

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        System.out.println(map.get("one"));  // 输出: 1
    }
}
2. HashMap的工作原理

HashMap使用哈希表来存储数据。键的哈希值通过hash()方法计算,然后通过哈希函数将哈希值映射到数组的索引位置上。通过链地址法(chaining)来解决哈希冲突,即在每个数组索引处存储一个链表(Java 8及之后版本采用红黑树以提高性能)。

public int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3. HashMap的常用操作
  • 添加元素: 使用put()方法。

  • map.put("four", 4);
  • 获取元素: 使用get()方法。

    int value = map.get("two");
  • 移除元素: 使用remove()方法。

    map.remove("three");
  • 遍历元素:

    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    

4. HashMap的优缺点

优点:

  • 快速查找: 平均时间复杂度为O(1)。
  • 灵活: 可以存储不同类型的对象,允许null键和值。

缺点:

  • 非线程安全: 多线程情况下需要手动同步。
  • 不保证顺序: 插入顺序和遍历顺序可能不同。

总结

HashMap是Java中一个强大且高效的集合类,用于快速查找和存储键值对。理解其工作原理和常用操作对于提高编程效率和解决复杂问题非常有帮助。

常见面试题
  1. HashMap的底层实现原理是什么?
  2. 如何解决HashMap中的哈希冲突?
  3. HashMapHashtable的区别是什么?
  4. 在什么情况下HashMap会发生扩容?
  5. 为什么HashMap不是线程安全的?如何实现线程安全的HashMap

常见面试题解答

1. HashMap的底层实现原理是什么?

HashMap的底层是基于哈希表(hash table)实现的。它内部使用一个数组来存储元素,每个数组的元素被称为“桶”(bucket)。当我们向HashMap中插入一个键值对时,会先根据键的hashCode()方法计算出哈希值,然后通过哈希函数将哈希值映射到数组的索引位置上。HashMap通过链地址法(chaining)来解决哈希冲突,即每个桶中存储一个链表(Java 8及之后版本采用红黑树以提高性能)。

public int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2. 如何解决HashMap中的哈希冲突?

HashMap采用链地址法(chaining)来解决哈希冲突。具体方法是,每个桶中存储一个链表(或者在Java 8及之后版本中,当链表长度超过一定阈值时,会转换成红黑树),所有映射到同一索引位置的键值对都会存储在这个链表或红黑树中。

当插入一个新的键值对时,如果该键值对的哈希值映射到的索引位置已经存在其它元素,则会将新的键值对添加到该位置的链表或红黑树中。

3. HashMap和Hashtable的区别是什么?
  • 线程安全性: Hashtable是线程安全的,所有方法都是同步的,而HashMap不是线程安全的,适用于单线程环境或通过外部同步来保证线程安全。
  • null键和值: HashMap允许一个null键和多个null值,而Hashtable不允许null键和值。
  • 性能: 由于Hashtable的方法是同步的,因此在单线程环境下性能比HashMap差。
  • 遗产: Hashtable是基于较老的Dictionary类实现的,而HashMap是从Java 1.2开始作为Map接口的实现类。
4. 在什么情况下HashMap会发生扩容?

HashMap会在容量达到阈值(默认是当前容量的0.75倍)时发生扩容。扩容时,HashMap的容量会变为原来的两倍,并重新哈希已有的键值对,重新分配到新的桶中。扩容可以避免哈希冲突,保持HashMap的高效性。

5. 为什么HashMap不是线程安全的?如何实现线程安全的HashMap?

HashMap不是线程安全的,因为它的所有方法都不是同步的。在多线程环境下,多个线程同时修改HashMap的结构可能导致数据不一致或出现死循环。

要实现线程安全的HashMap,可以通过以下方法:

  • 使用Collections.synchronizedMap(Map<K, V> m)方法: 这个方法返回一个线程安全的Map

    Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
  • 使用ConcurrentHashMap 这是Java提供的线程安全的Map实现,适用于高并发环境。它通过分段锁机制(Segmented Locking)来提高并发性能。

    ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

HashMap源码

1 put方法流程

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组是否未初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        //如果未初始化,调用resize方法 进行初始化
        n = (tab = resize()).length;
    //通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果没有,直接将数据放在该下标位置
        tab[i] = newNode(hash, key, value, null);
    //该数组下标有数据的情况
    else {
        Node<K,V> e; K k;
        //判断该位置数据的key和新来的数据是否一样
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到
            e = p;
        //判断是不是红黑树
        else if (p instanceof TreeNode)
            //如果是红黑树的话,进行红黑树的操作
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //新数据和当前数组既不相同,也不是红黑树节点,证明是链表
        else {
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                //判断next节点,如果为空的话,证明遍历到链表尾部了
                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;
                }
                //判断链表当中有数据相同的值,如果一样,证明为修改操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //把下一个节点赋值为当前节点
                p = e;
            }
        }
        //判断e是否为空(e值为修改操作存放原数据的变量)
        if (e != null) { // existing mapping for key
            //不为空的话证明是修改操作,取出老值
            V oldValue = e.value;
            //一定会执行  onlyIfAbsent传进来的是false
            if (!onlyIfAbsent || oldValue == null)
                //将新值赋值当前节点
                e.value = value;
            afterNodeAccess(e);
            //返回老值
            return oldValue;
        }
    }
    //计数器,计算当前节点的修改次数
    ++modCount;
    //当前数组中的数据数量如果大于扩容阈值
    if (++size > threshold)
        //进行扩容操作
        resize();
    //空方法
    afterNodeInsertion(evict);
    //添加操作时 返回空值
    return null;
}

2 扩容

//扩容、初始化数组
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
    	//如果当前数组为null的时候,把oldCap老数组容量设置为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //老的扩容阈值
    	int oldThr = threshold;
        int newCap, newThr = 0;
        //判断数组容量是否大于0,大于0说明数组已经初始化
    	if (oldCap > 0) {
            //判断当前数组长度是否大于最大数组长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果在最大长度范围内,则需要扩容  OldCap << 1等价于oldCap*2
            //运算过后判断是不是最大值并且oldCap需要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold  等价于oldThr*2
        }
    	//如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,       			如果是首次初始化,它的临界值则为0
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //数组未初始化的情况,将阈值和扩容因子都设置为默认值
    	else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	//初始化容量小于16的时候,扩容阈值是没有赋值的
        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;
    	//扩容操作,判断不为空证明不是初始化数组
        if (oldTab != null) {
            //遍历数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判断当前下标为j的数组如果不为空的话赋值个e,进行下一步操作
                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 {
                        //比如老数组容量是16,那下标就为0-15
                        //扩容操作*2,容量就变为32,下标为0-31
                        //低位:0-15,高位16-31
                        //定义了四个变量
                        //        低位头          低位尾
                        Node<K,V> loHead = null, loTail = null;
                        //        高位头		   高位尾
                        Node<K,V> hiHead = null, hiTail = null;
                        //下个节点
                        Node<K,V> next;
                        //循环遍历
                        do {
                            //取出next节点
                            next = e.next;
                            //通过 与操作 计算得出结果为0
                            if ((e.hash & oldCap) == 0) {
                                //如果低位尾为null,证明当前数组位置为空,没有任何数据
                                if (loTail == null)
                                    //将e值放入低位头
                                    loHead = e;
                                //低位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    loTail.next = e;
                                //记录低位尾数据
                                loTail = e;
                            }
                            //通过 与操作 计算得出结果不为0
                            else {
                                 //如果高位尾为null,证明当前数组位置为空,没有任何数据
                                if (hiTail == null)
                                    //将e值放入高位头
                                    hiHead = e;
                                //高位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    hiTail.next = e;
                               //记录高位尾数据
                               	hiTail = e;
                            }
                            
                        } 
                        //如果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;
    }

3 get方法

public V get(Object key) {
    Node<K,V> e;
    //hash(key),获取key的hash值
    //调用getNode方法,见下面方法
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}


final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //找到key对应的桶下标,赋值给first节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //判断hash值和key是否相等,如果是,则直接返回,桶中只有一个数据(大部分的情况)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        if ((e = first.next) != null) {
            //该节点是红黑树,则需要通过红黑树查找数据
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            //链表的情况,则需要遍历链表查找数据
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

觉得有用的话可以点点赞 (*/ω\*),支持一下。

如果愿意的话关注一下。会对你有更多的帮助。

每天都会不定时更新哦  >人<  。

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

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

相关文章

Simulink 模型生成 C 代码(四):比较模型仿真和生成代码的结果

接下来将验证生成的代码执行时在数值上等效于 Simulink 中建模的算法。您使用测试框架模型在普通模式下对 RollAxisAutopilot 进行仿真&#xff0c;并在 SIL 模式下进行仿真&#xff0c;然后使用仿真数据检查器比较这两个仿真。 要测试生成的代码&#xff0c;您可以运行软件在…

实验六 智能停车系统设计 (综合类)含源码 福利

某停车场停车费用计算规则如下: ①每小时 10 元&#xff0c;不足 1 小时的部分按照 1 小时计算; ②超过 8 小时&#xff0c;未超过 24 小时的按照 8 小时计算; ③超过 24 小时&#xff0c;超过部分按照上述标准重新计算。 本程序的任务是模拟一个智能停车管理系统&#xff0c;…

qt5.15关于qradiobutton遇到的坑

前言 不知道是只有我遇到了&#xff0c;还是qt本身就存在这个bug 当将2个qradiobutton放入到一个布局内&#xff0c;然后进行来回切换&#xff0c;若无数据刷新的情况下&#xff0c;切换无异常&#xff0c;当窗体内有数据开始刷新了&#xff0c;则点击其中一个qradiobutton&am…

考PMP一定要报培训班么?

曾有自学PMP想法学员分享&#xff1a;不如选择性价比高通过率高的PMP项目管理培训机构威班PMP 其实参加PMP考试如果非要自学也能参加考试的&#xff0c;只是需要找一个能卖给你35学时的机构&#xff0c;也只有PMI授权的PMP机构能开具35学时证明&#xff0c;这种生意也只有小机…

工厂自动化相关设备工业一体机起到什么作用?

在当今的制造业领域&#xff0c;工厂自动化已成为提高生产效率、保证产品质量和降低成本的关键。在这一进程中&#xff0c;工业一体机作为一种重要的设备&#xff0c;发挥着不可或缺的作用。 工业一体机是自动化生产线上的控制中心。它能够整合和处理来自各个传感器、执行器和其…

Hadoop3:集群压测-读写性能压测

一、准备工作 首先&#xff0c;我们要知道&#xff0c;平常所说的网速和文件大小的MB是什么关系。 100Mbps单位是bit&#xff1b;10M/s单位是byte ; 1byte8bit&#xff0c;100Mbps/812.5M/s。 测试 配置102、103、104虚拟机网速 102上用Python开启一个文件下载服务&#x…

没有找到openslide-win64xxxx文件 ! ! ! (openslide-python安装教程)

各位小伙伴大家好&#xff0c;今天给大家带来教程&#xff1a;openslide-python安装 说实话这个库我之前也没有用到过&#xff0c;然后今天代码需要&#xff0c;就安装了一下 但是在import openslide的时候报错&#xff0c;找了很多教程 说句心里话&#xff1a;那些教程都是…

又一个被催的相亲对象!家庭不和,是因为智慧不够?——早读(逆天打工人爬取热门微信文章解读)

你相亲过吗&#xff1f; 引言Python 代码第一篇 洞见 家庭不和&#xff0c;是因为智慧不够第二篇 口播结尾 引言 yue 昨天居然忘记了 正事&#xff1a;拍视频j 居然忘记了 别着急 让我找下理由&#xff08;借口&#xff09; 前天我妈给我介绍了个相亲对象 推给我了她的微信 我…

基于opencv-python开发的长度测量-角度测量算法

使用OpenCV-Python进行长度和角度测量的项目可以应用于多个领域&#xff0c;如工业自动化、机器人视觉、测绘、教育等。这类项目的核心是利用计算机视觉技术从图像或视频中提取有用的信息&#xff0c;进而计算出物体的尺寸或角度。以下是一个基于OpenCV-Python进行长度和角度测…

软考《信息系统运行管理员》-2.4信息系统运维管理标准

2.4信息系统运维管理标准 信息系统运维的相关标准 ITIL信息技术基础设施库 基于服务生命周期主要包含五个方面&#xff1a;服务战略&#xff08;轴心&#xff09;、服务设计、服务转换、服务运营及服务改进 COBIT信息系统和技术控制目标 考法1&#xff1a;概念 在ITILv3基于…

开源 复刻GPT-4o - Moshi;自动定位和解决软件开发中的问题;ComfyUI中使用MimicMotion;自动生成React前端代码

✨ 1: Moshi 法国 AI 实验室 Kyutai 刚刚推出了开源 复刻GPT-4o - Moshi Moshi是一款现代化聊天平台&#xff0c;旨在提供用户友好和高效的即时通讯体验。它整合了多种功能&#xff0c;包括文本消息、语音和视频通话、文件共享等&#xff0c;为个人用户和团队协作提供了强大的…

grid布局下的展开/收缩过渡效果【vue/已验证可正常运行】

代码来自GPT4o&#xff1a;国内官方直连GPT4o <template><div class"container"><button class"butns" click"toggleShowMore">{{ showAll ? 收回 : 显示更多 }}</button><transition-group name"slide-fade&…

Hadoop-11-MapReduce JOIN 操作的Java实现 Driver Mapper Reducer具体实现逻辑 模拟SQL进行联表操作

章节内容 上一节我们完成了&#xff1a; MapReduce的介绍Hadoop序列化介绍Mapper编写规范Reducer编写规范Driver编写规范WordCount功能开发WordCount本地测试 背景介绍 这里是三台公网云服务器&#xff0c;每台 2C4G&#xff0c;搭建一个Hadoop的学习环境&#xff0c;供我学…

【10年有效】阿里云域名,出阿里云私人子域名

出&#xff1a;阿里云私人子域名&#xff0c;主要是帮助没域名的&#xff0c;又需要使用域名绑定程序的人。 有效期十年&#xff0c;就只要几块&#xff0c;简直是薅羊毛薅到家了~~ 本域名已经备案了。 目标&#xff1a;https://h5.m.goofish.com/item?id811115711415 ---…

【楚怡杯】职业院校技能大赛 “Python程序开发”赛项样题二

Python程序开发实训 &#xff08;时量&#xff1a;240分钟&#xff09; 中国XX 实训说明 注意事项 1. 请根据提供的实训环境&#xff0c;检查所列的硬件设备、软件清单、材料清单是否齐全&#xff0c;计算机设备是否能正常使用。 2. 实训结束后&#xff0c;将各试题代码整合…

QQ录屏文件保存在哪里?一键教你快速查询

无论是记录重要的工作内容&#xff0c;还是分享生活中的点滴&#xff0c;屏幕录制都发挥着至关重要的作用。在众多屏幕录制工具中&#xff0c;qq录屏以其简单易用、功能丰富的特点&#xff0c;受到了广大用户的喜爱。本文将为您揭示qq录屏文件保存在哪里&#xff0c;帮助大家更…

DAY18-力扣刷题

1.从前序与中序遍历序列构造二叉树 105. 从前序与中序遍历序列构造二叉树 - 力扣&#xff08;LeetCode&#xff09; 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树…

C# 实现位比较操作

1、目标 对两个字节进行比较&#xff0c;统计变化位数、一位发生变化的位数、二位发生变化的位数、多位发生变化的位数。 2、代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Lin…

大模型时代数据库技术创新

本文整理自 2024 年 6 月 ArchSummit&#xff08;深圳站&#xff09; Data4AI 和 AI4Data 方面的探索和实践案例专题的同名主题分享。 大家好&#xff0c;我今天讲的内容总共分为三部分&#xff0c;先是数据库和大模型的演变历程&#xff0c;尤其是两者的结合的过程。然后在分别…

高浓度锡回收的工艺流程

高浓度锡回收的工艺流程是一个复杂而精细的过程&#xff0c;它旨在从废旧锡制品或含锡废料中高效、环保地提取出高纯度的锡。以下是对该工艺流程的详细阐述&#xff1a; 一、收集与预处理 收集&#xff1a;高浓度锡回收的第一步是收集废旧锡制品或含锡废料&#xff0c;这些材料…