Java集合List、Set、Map底层源码解析

news2025/2/25 17:16:10

目录

    • 集合介绍
    • Map
      • HashMap
      • LinkedHashMap
      • HashTable
      • Properties
    • Collection
      • List
        • ArrayList
        • Vector
        • LinkedList
      • Set
        • HashSet
        • LinkedHashSet
        • TreeSet

集合介绍

为什么使用集合?
当我们想要保存一组数据时,可以使用到的变量类型有集合和数组。那么就像说一下数组的局限性:

  1. 数组一但类型确定,整个数组就只能放置此类型的数据,当然可以指定为类型为Object。
  2. 数组长度固定,当数据超出预先设定的大小时就会出错。有些集合类型虽然底层采用的也是数组,但集合对数组自动进行了管理,自动进行扩容,拷贝。
  3. 数组选择性少,无法根据数据使用情况选择合适的数据结构。数组就是一种数组数据结构,数组数据结构的特点就是一块连续的地址空间,可以很快的根据序号计算出各个元素的内存地址,但如果在中间插入数据删除数据就需要将后面的数据依次地移动,因此对于一些频繁发生中插入和中删除的数据就不能选用数组。而集合采用的类型可以有很多,例如:linkedList就是采用双向链表的方式。数组只有一种类型进行选择,而集合对于数据频繁发生删除和增加可以选择链表类型结构。

数组比较原始,而集合功能比较丰富,有的集合底层就是个数组,是在数组基础上的一些功能的增强。数组只是一种数据类型,而集合可以使用的数据类型更多,像链表、数组、哈希表、红黑树等

集合分为单列集合和双列集合,单个值为一组的就是单列集合,K_V为一组的就是双列集合。
单列集合又分为两个大类,一个List,一个是Set,双列集合只有Map(k_V键值对方式,K不可重复)一个大类
为什么每种集合的功能不一样呢?因为他们要么底层数据结构不同,要么扩容机制不同,要么安全机制不同等。

一个种类的集合按照需求不同分为了许多种,这里只介绍具常用到的几种类型。注意乡民这个图只是进行一下分类而不是表示类和类的父子关系
在这里插入图片描述
在读源码时发现一个特点:源码中真的很喜欢在判断,循环时进行赋值

Map

HashMap

HashMap的构造方法
在这里插入图片描述
当添加一组K_V键值对时

先了解HashMao底层是怎样存储的
当放入一组key_value键值对时,先将key进行hash运算,然后根据hash运算结果除以底层Node<k_v>数组的长度计算出存储在Node<k_v>的位置,如果此位置已经有值,就向后存储,使得在Node<k_v>数组相同位置的节点形成一个单链表的结构。当链表数量超过8时,就对Node<k_v>数组进行扩容,如果已经扩容数组长度已经到64长度就将链表进行树化,转为红黑树
在这里插入图片描述
下面将详细介绍这一过程:
先将key判断是否为null,为null则值为0,不为null则获取它的hashCode右移16位
在这里插入图片描述
每一组k_v键值对底层都是存储在Node<k_v>中的,而Node<k_v>继承了Map.Entry<k_v>
在这里插入图片描述
具体的存储过程
在这里插入图片描述

HashMap的底层是一个名为table的数组,当计算出的数组坐标已经有值时,就会以链表的形式向后存储。如果发现与自己hash值页相同的key,就将旧值取出然后使用新值覆盖掉。在此期间共发生两次扩容,一次是当table数组长度为0时,一个是当table数组长度(每次添加size就会++,所以算上了链表长度)达到警戒值时。那么扩容是怎样过程呢?这就需要查看resize()方法
在这里插入图片描述
以上是当table表的总数超过阈值时或第一次向空table数组放入新值时对table数组的扩容,当单个链表的总数超过8也会进行一次树化
在这里插入图片描述
在这里插入图片描述
那么为什么HashMap要采用数组+链表+红黑树这种存储方式呢?数组的特点是查找快,长度有限,但使用hash来确定数组角标容易产生hash碰撞,这就不得不使用链表。在根据key查找value时,先对key进行hash计算,这样就可以快速的定位但数组的某个位置。但这个位置可能有多个节点,因此就需要沿着链表依次比对key。但当这个链表过长时,就会使得在链表上查找花费的时间过长。因此当链表数量达到8时,要么对数组进行扩容,减少hash碰撞的概率,要么将链表进行树化,提升key的查找效率。

hashMap是如何根据key来查找元素的呢?
在这里插入图片描述
思考:如果判断是否是要查找的节点的呢?
源码中已经给出,Hash值要相同(在添加节点时已经放入Node节点中),key要相同。HashMap的key不仅可以使用String类型,还可以使用对象。
在这里插入图片描述
那么要想使得cat1对象能够覆盖掉cat2对象,或者是同一个类型的对象在map集合中只能有一个,或者只根据某个属性判断是否为同一个key,要怎么操作呢?
首先就要明白是如和判断key相同的,前面已经介绍过:hash值相同在比较key是否相同或相等。那么这就只需要重写每个类的HashCode和equals方法,使其能够按照我们的想法对判断是否为同一个key。
在这里插入图片描述

HashMap的几种遍历方式
为什么说HashMap的插入时和遍历时是无序的呢?因为HashMap再插入时,是根据计算key的Hash值来获取要插入的数组位置的,而不是按照先后顺序在数组中依次排列。在遍历时,是按照数组顺序进行遍历的,这就会导致虽然插入顺序不一样,但他们在数组中或者在链表中是相连的,使得遍历结果顺序相同。
在这里插入图片描述

Map集合可以将key单独放在一起作为一个keySet,将Value值放在一个作为Values,每一组key_value为Entry。但注意的是在无论是调用keySet()方法或Values()方法时,返回的值也是通过遍历Map获得的,不是又将key或Value再单独存一份。
例如:keySet源码
在这里插入图片描述
从这个源码中,可以知道KeySet可以为一个Set集合也可以为一个对象,有size(),Clear()方法等。
使用KeySet()获取Keys,在通过Map查找key进行遍历
在这里插入图片描述
在这里插入图片描述

使用EntrySet()获取或者组k_v值
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

LinkedHashMap

LinkedHashMap继承了HashMap类。将Node<k_v>这种单链表的基础上,多了个before, after指针,一个节点指向单向链表下一个节点的同时,还指向了它的上一个插入元素和下一个要插入的元素。
在这里插入图片描述
在这里插入图片描述
这样就使得map的插入能有先后顺序,能在原数组和链表的基础上,通过第一个插入的节点顺着链表能一直找到最后一个节点
插入规则还是HashMap的方法,只是将Node节点更换
在这里插入图片描述
相同与Hashmap中维护着两条互不相干的结构,一个存储用的链表/树,一个是用来遍历的链表,由first指向头,last指向尾
在这里插入图片描述
在插入一个节点时,将原最后一个节点由新节点的before指针指向,同时将自己的节点赋值为tail,原最后一个节点的after指针指向最新插入的节点。这样在原HashMap的基础上,由插入顺序形成一个形成的单链表,使得插入有序
当LinkedHashMap执行keySet()时,原HashMap是直接遍历table,然后遍历链表,那么看看作为有序LinkedhashMap是如何进行遍历的呢?
在这里插入图片描述
LinedHashMap与hashMap遍历对比
在这里插入图片描述
值的注意的是:LinkedHashMap与HashMap很像,LinedhashMap只是多个按照插入顺序形成的链表,两者在遍历方式外其余相差无几二者都是不安全的

HashTable

Hashtable的构造时就已经对table数组完成初始化
在这里插入图片描述
接下来让我们看一下HashTable的插入方式 HashTable的扩容机制与插入方式很特殊
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
思考Hashtable单纯在put数据时与HashMap有哪些不同呢?

  • HashTable在构造时就指定table的初始大小,默认为11,手动指定最少为1
  • HashTable计算存储数组角标是(hash & 0x7FFFFFFF) % tab.length而hashMap是 (n - 1) &key.hashCode()) ^ (h >>> 16)
  • HashTable是线程安全的,put方法有synchronized修饰,HashMap就是线程不安全的
  • HashTable插入数据时,是插入链表的前面,将此数组以前的空值也好,单个Entry也好,Entry链表也好,都放入插入节点的next后面
  • HashTable的Value在插入时不能为空。
  • Hashtable使用count记录节点数量,HashMap使用size记录
  • 扩容时机于条件不同
    在这里插入图片描述

相同部分

  • 阈值比例依旧是0.75,但Hashtable在构造时可以指定

上面源码中当count >= threshold是会进行 rehash()也就是扩容,那么接下来介绍一下扩容的源码
在这里插入图片描述
扩容方面与HashMap不同

  • Hashtable扩容需要对数据重新移动位置
  • HashTable对链表的长度没有限制,没有这一说

Properties

Properties继承了HashTable,与Hashtable很像,更适合用来读取配置文件并将其转为hashTable

Collection

Collection是一个单向集合的接口,在接口中定义了很多方法为所有单向集合共有的功能。
在这里插入图片描述

List

ArrayList

ArrayList底层是一个名为elementData的数组,在创建ArrayList集合时,提供有参构造和无参构造
如果直接使用new ArrayList(),这种就是无参构造,在构造器的内部会为elementData数组赋值一个Object类型的空数组
无参构造创建ArrayList对象
在这里插入图片描述
在构造ArrayList时,传入Collection集合,就会在将此集合完成转换,并在此集合基础上进行操作。
在这里插入图片描述

在创建ArrayList对象时指定初始数组大小
在这里插入图片描述

在向数组中初始添加元素时,会将elementData数组扩容至10
在这里插入图片描述
那么当扩容的数组容量用完了怎么办?1.5倍扩容
在这里插入图片描述
如果扩容后已经超出MAX_ARRAY_SIZE大小,那么就不在采用1.5倍的扩容方式,而是直接返回Integer.MAX_VALUE
在这里插入图片描述
总结ArrayList的扩容方式
在这里插入图片描述
s就是数据要存储时数组的坐标,如果s已经与数组大小相等就代表已经没有地方去存储新的数据了,初始时,s=0,elementData的长度为0,已经无法存储数据,需要扩容,扩容后,elementData[s]=当前数据,然后s++。当数组中添加完最后一个元素,s++=10时,就代表下一个元素是第11个数据,此时s=elementData.length,就需要再次扩容了。

Vector

无参创建Vector对象
Vector的底层也是采用的名为elementData的数组,对于数组的初始大小不像ArrayList那样:在添加元素时进行判断,判断是否是空数组扩容,如果是就扩容10个单位,Vector的如果是无参构造,在无参构造中就调用有参构造,将参数设置为10
在这里插入图片描述在创建对象时,就已经创建了创建了指定长度的Object型elementData数组,这点和上面的ArrayList很不相同
在向数组中初始添加元素时,与ArrayList不同的是,它是线程安全的。
在创建Vector时,可以指定每次扩容的大小,如果不指定,那么默认为在原数组大小基础上再加上同样大小
底层数组满了怎么办?2倍扩容
在这里插入图片描述
那么如果在构造Veactor时手动指定elementData大小为0时,怎么办?
如果觉得创建Vector时默认就指定10个大小长度有些不妥,可以在创建对象时手动指定长度为0,这样elementData的长度就为0。在添加元素时就行扩充
在这里插入图片描述

Vector与ArrayList不同之处

  • Vector是线程安全的,在add()方法前添加了synchronized,而ArrayList没有
  • Vector在无参构造创建对象是就开辟了elementData数组的大小,而ArrayList在无参构造创建对象时elementData数组的大小为空,只有在添加元素时才会进行初始化
  • Vector可以指定每次扩容大小,在有参构造的第一个参数指定elementData大小,第二个参数就是执行每次扩容大小。而ArrayList不能够指定,每次大小固定。
  • Vector若不指定每次扩容大小,则扩容时是新数组长度=原数组长度+原数组长度相当于是2倍,而ArrayList是新数组长度=原数组长度+0.5原数组长度相当于是1.5倍
  • ArrayList在构造时,可以赋值为一个集合的数据
    在这里插入图片描述

Vector与ArrayList的相同之处

  • 底层都采用名为elementData的数组
  • 触发扩容机制相同,都是当s==elementData.length时,调用扩容方法。扩容后将s作为新数据的数组的脚标,然后s++
    在这里插入图片描述
    Vector适用场景
    如果对线程安全有要求,就使用Vector,如果想要手动指定每次扩容大小,就使用Vector。

总结
在这里插入图片描述

LinkedList

LinkdList底层采用的是双向链表,以上两个都是数组。链表和数组的不同就是,链表十分的灵活,添加元素只需要创建单个节点,然后将指针指向它即可,而数组需要提前开辟一段空间,这就使得LinkedList的构造方法与ArrayList、Vector都不相同,它不需要开辟空间。
值的注意的是:虽然java中没有指针的概念,但指针是真实存在的。例如:String a=new String(“test”),a指向的就是堆空间中的String对象地址。
这里使用Java来写一个简单的单向链表
在这里插入图片描述

LinkedList的构造方法
在这里插入图片描述
但当向LinkedList中添加数据时

在这里插入图片描述
将原来last执向的节点记录下来,然后将last指向新添加的节点。判断此节点是否是第一个节点,就要看原last指向节点是否为空,如果原last指向节点为空,那么此节点就是第一个节点,将执行第一个节点的first执行它。如果原last指向的节点不为空,那么代表新添加的节点前还有节点,因为在创建新节点时,已经将新节点的pred指针指向last指向的位置了,但对于上一个节点,还没有将next指向新添加的节点。
那么双向链表在移除元素时是怎样进行操作的?

在这里插入图片描述

Set

HashSet

HashSet的本质就是一个HashMap,HashSet中的每个值都对应HashMap的key,并且它的value设置为空对象
这也就解释了HashSet的两大特性:

  1. 不重复:因为HashMap的key不允许重复
  2. 无序:说是无序准确来说是没有按照插入顺序,因为HashMap底层根据Hash值计算出数据在table数组(Node类型)中位置的,而不是按照插入顺序。

接下来介绍一下HashSet的源码,但因为前面已经介绍过HashMap,所以相同的部分这里不再赘述
HashSet的构造 HashSet的构造有很多,这里只介绍一下无参构造,有参构造要么是在指定阈值比例,要么就是table的初始化大小
在这里插入图片描述
当添加数据时add()
在这里插入图片描述

LinkedHashSet

LinkedHashSet同样底层是LinkedHashMap,那么LinkedhashMap相较于HashMap有什么特点呢?最大的特点就是单独维护一条链表使得HashMap的可以按照插入顺序进行输出。所以LinkedhashSet与HashSet最大的不同也是,LinkedHashMap是有序的,但Key依然不可重复
在这里插入图片描述
当添加一个元素时
在这里插入图片描述

TreeSet

TreeSet的底层使用的TreeMap,TreeMap的最大特点就是底层是一个红黑树,可以对插入key进行排序,当然要想自定义可以传入Comparator方法
在这里插入图片描述
TreeSet同样将值作为TreeMap的key,可以对Set中的值进行排序。
在这里插入图片描述

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

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

相关文章

1.9

完善案例 ​ this.$nextTick(function () {this.$refs.input.focus()})}​ //点击编辑直接获取焦点 this.$refs.input.focus() //但是这种写法 由于input是用v-show控制的 所以执行了上面代码 改变了isEdit值 但是不会立马去重新解析模板 input框还没有出来 //将handleedit回调…

springboot+xxl-job初体验

在日常开发中经常遇到任务调度的需求&#xff0c;下面体验一下分布式调度xxl-job。 1、常见定时任务的实现方式 Java自带的java.util.Timer类ScheduledExcutorServiceSpringboot自带的EnableScheduling注解Quartz框架Elastic-job框架XXL-JOB框架 下面对xxl-job实现任务调度尝…

【TypeScript】TS进阶-函数重载(七)

&#x1f431;个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️作者简介&#xff1a;前端领域新星创作者、华为云享专家、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab;系列专栏&#xff…

一文读懂电商数字员工究竟是什么?| RPA铺第1期

百度的曦灵&#xff0c;中核的核小智、万科集团数字员工“崔筱盼”都曾在2022年度爆火&#xff0c;数字员工一夜之间成为众多行业追捧的对象&#xff0c;那么&#xff0c;他/她们究竟代表着什么&#xff0c;对企业而言&#xff0c;是噱头大于收益&#xff0c;还是收益大于噱头呢…

简单的客户端服务器(多线程)基于json通信

这是一个客户端和服务器通信的小项目&#xff0c;小编自己手写了一个json工具&#xff0c;测试比jsoncpp&#xff0c;protobuf "效率更高"&#xff08;浅拷贝&#xff09;&#xff0c;改写成深拷贝效率会急剧下降&#xff08;小编能力有限&#xff0c;知识点&#xf…

技术分享 | Spring Boot 异常处理

说到异常处理&#xff0c;我们都知道使用 try-catch 可以捕捉异常&#xff0c;可以 throws 抛出异常。那么在 Spring Boot 中我们如何处理异常&#xff0c;如何更优雅的处理异常&#xff0c;如何全局处理异常。是本章讨论解决的问题。Java 异常类首先让我们简单了解或重新学习下…

Android开发-AS学习(一)

完整项目最精简流程&#xff1a;Android <-> 接口 <-> 数据库Android开发工具&#xff1a;Android Studio项目目录信息&#xff1a;一、控件1.1 TextView基础属性描述layout_width组件的宽度layout_height组件的高度id为TextView设置一个组件idtext设置显示的文本内…

Linux 学习笔记(借鉴黑马程序员Linux课程)

Linux视频课程 简介 Linux诞生于1991年&#xff0c;由林纳斯托瓦兹在21岁时完成。此后成为最为流行的服务器操作系统之一。 Linux内核和系统发行版 由Linux系统内核和系统级应用程序两部分组成。 内核提供系统最核心的功能&#xff0c;如&#xff1a;调度CPU、调度内存、调…

【笑小枫的按步照搬系列】Redis多系统安装(Windows、Linux、Ubuntu)

笑小枫&#x1f495; 欢迎来到笑小枫的世界&#xff0c;喜欢的朋友关注一下我呦&#xff0c;大伙的支持&#xff0c;就是我坚持写下去的动力。 微信公众号&#xff1a;笑小枫 笑小枫个人博客&#xff1a;https://www.xiaoxiaofeng.com Windows 下安装 方式一&#xff1a;https…

HCL(新华三模拟器)如何连接CRT及改界面颜色

我使用的HCL版本是2.1.1官网下载连接&#xff1a;https://www.h3c.com/cn/Service/Document_Software/Software_Download/Other_Product/H3C_Cloud_Lab/Catalog/HCL/&#xff0c;一键直达下载官网这个版本可以适配virtualbox的5.2.44的版本&#xff0c;同时支持华为的ensp模拟器…

Spring Security 表单配置(一)

Spring Security 表单配置&#xff08;一&#xff09;工具版本及环境参考资源配置详细说明工具版本及环境 工具&#xff1a; IDEA 2021.3 Maven 3.8.6 JDK 8 Spring Boot 2.7.7 Spring Securiity 5.7.6 参考资源 Spring Security官网: https://docs.spring.io/spring-security…

javaEE 初阶 — 常见的锁策略

文章目录1. 乐观锁 vs 悲观锁2. 互斥锁 vs 读写锁3. 重量级锁 vs 轻量级锁4. 自旋锁 vs 挂起等待锁5. 公平锁 vs 非公平锁6. 可重入锁 vs 不可重入锁7. synchronized 是哪种锁8. 相关面试题1. 乐观锁 vs 悲观锁 乐观锁 指的是预测锁竞争不是很激烈&#xff0c;也就是指这里做的…

排列组合两个列表中的元素 product(a,b)

【小白从小学Python、C、Java】【计算机等级考试500强双证书】【Python-数据分析】排列组合两个列表中的元素product(a,b)关于以下python代码表述错误的一项是?from itertools import producta [1,2,3]b [4,5,6]print("【显示】a",a)print("【显示】b",…

修改Keil开发环境背景(修改为黑色主题)

修改Keil开发背景&#xff08;修改为黑色主题&#xff09;引言步骤总结引言 我们平时在Keil uVision5下开发单片机/嵌入式程序的时候&#xff0c;如果始终用的是官方的主题背景&#xff0c;往往会因为背景太白了&#xff0c;而导致视疲劳。我们使用VSCode往往不会有这种困扰&a…

Python处理字符串 中文(汉字)编码示例

在实际开发中&#xff0c;发现打印Python打印经常出现乱码&#xff0c;大部分都是编码引起&#xff0c;这里只是简单说一下utf-8/ gbk/ unicode编码之间的相互转换问题&#xff1a; utf-8是Unix下的一种通用编码&#xff0c;gbk是win环境下的一种汉字编码&#xff0c;unicode是…

计算机组成原理_总线仲裁

计算机组成原理总目录总线仲裁 由于总线连接着多个设备&#xff0c;那么在同一时刻多个设备想要占用总线时&#xff0c;就会发生冲突问题 为了解决冲突问题&#xff0c;就需要规定好所有设备使用设备的优先权&#xff0c;仲裁也就是判断设备占用总线的优先权 例如设备A的任务比…

Docker--安装mysql8--方法/步骤

原文网址&#xff1a;Docker--安装mysql8--方法/步骤_IT利刃出鞘的博客-CSDN博客 简介 本文介绍如何使用docker安装mysql8。 查看mysql版本 https://hub.docker.com/_/mysql/tags 我选择8.0&#xff0c;点击右侧箭头处直接复制命令。 拉取镜像 拉取镜像的命令 docker pul…

RADIANT:一种全新的雷达图像关联网络用于3D目标检测

RADIANT&#xff1a;一种全新的雷达图像关联网络用于3D目标检测 论文地址&#xff1a;RADIANT: Radar-Image Association Network for 3D Object Detection 源码&#xff1a;https://github.com/longyunf/radiant 单位&#xff1a;密歇根大学、福特 动机 作为一种能够直接…

全景剖析阿里云容器网络数据链路(二):Terway EN

作者&#xff1a;余凯 本系列文章由余凯执笔创作&#xff0c;联合作者&#xff1a;阿里云容器服务 谢石 对本文亦有贡献 前言 近几年&#xff0c;企业基础设施云原生化的趋势越来越强烈&#xff0c;从最开始的 IaaS 化到现在的微服务化&#xff0c;客户的颗粒度精细化和可观…

使用小度音箱+Blinker控制ESP01S Relay继电器模块

一. 使用ESP01S模块&#xff0c;PIN脚定义如下&#xff1a; 管脚功能如下&#xff1a; ESP01S模块原理图&#xff1a; ESP01S模块比ESP01模块做了以下优化&#xff1a; LED灯的管脚发生变化&#xff0c;由ESP01的TXD0变成ESP01s的GPIO2引脚&#xff1b;ESP01s模块的IO0、RST、…