Java集合—Set(Collection子接口)及其子类(HashSet、LinkedHashSet)包括HashMap源码分析

news2024/11/16 1:55:22
Set接口是 Collection接口的子接口。
1、无序,即添加元素和去除元素的顺序不一致。
但是每次取出的顺序是一致的。
2、不允许重复元素,可以有null,但只能有一个。
3、实现类很多,主要介绍HashSet、LinkedHashSet 和 TreeSet。
常用方法
因为其为Collection的子接口,因此常用方法和Collection一致。
遍历方式
因为其是Collection的子接口,所以其遍历方法与Collection一致。
即:
  1. 使用迭代器Iterator
  2. 使用增强for

一、HashSet

  1. HashSet实现了Set接口
  2. HashSet底层上是HashMap,其底层为:数组+链表+红黑树
  3. 可以存放null,但是只能存放一个。
  4. HashSet不保证数据是有序的(即不保证存取顺序一致),取决于hash后,再确定索引的结果。
  5. 不能有重复的对象

源码

  • HashSet底层是HashMap
  • 添加一个元素时,先得到hash值,将其转变为索引值
  • 找到存储数据表table,判断该索引位置是否已经有存放的元素
    • 没有,则直接加入
    • 有,则调用 equals 比较,相同则放弃添加;不同,则添加到链表后面
  • 在Java8中,如果一条链表个数到达了TREEIFY_THRESHOLD(默认值为8),
           并且table>=MIN_TREEIFY_CAPACITY(默认64),就会转化成红黑树。
1、新建对象
    调用自身构造器,在构造器内调用HashMap()构造器。
    
  
2、add()添加对象
    执行add()方法,调用put()方法
    然后执行put()方法,调用hash()获取 key值 ,之后调用putVal()方法
    putVal()则是真的存放数据的方法。
①add()方法
    可见,add()方法直接调用 put()方法,传入两个参数,分别为key和value;
        key为我们add的数据;
        value为PRESENT,下面第二张图,可见其为一个静态的常量,为所用对象共有的
        因为其底层是HashMap,是键值对,其中key为HashSet要存放的数据,Value如果填Null,虽然节省空间,
        但是意味着无论是否添加成功,都返回Null,那么则无法判断是否添加成功,因此放了 PRESENT。
        可见, 返回null才是添加成功
    
    
②HashMap.put()方法:
    put()方法直接先调用hash()获取hash值,然后调用putVal()方法。
    
    hash()将我们 即将添加的值hashcode值 与 其hashcode无符号向右位移16位的值 按位异或。
    目的就是为了减少碰撞, 具体原因: h = key.hashCode()) ^ (h >>> 16)  
    
    hashcode()方法获取hash值:
        不同类不一样,一般都会重写。
    Object类:    
        Object的hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址。
        注意其为native修饰。详情: Object顶级类(包含==和equals区别)
        
  
③HashMap.putval()方法:
源码前,先看:
    table为HashMap存放数据的数组,为Node类型的数组;
    
    Node为每个数据所存放的对象:
        hash:存放对象Key所对应的hash值
        key:存放数据对象
        value:每个对象都一样,就是①中所说的PRESENT,一个Object类型的常量对象。
        next:是下一个节点的引用。
    
    resize()数组扩容源码
final Node<K,V>[] resize() {
    //将table赋值给oldtab
    Node<K,V>[] oldTab = table;

    //oldcap存放旧table数组的大小
    //    如果旧数组为null,则oldcap为0
    //    如果旧数组不为null,则oldcap为其原来的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //oldthr存放扩容阈值,HashMap并不是数据满了才存放,而是达到阈值就进行扩容
    int oldThr = threshold;

    //newcap存放新的数组大小,newthr存放新的扩容阈值
    int newCap, newThr = 0;

    
    //oldcap大于0,即table扩容前不为null的情况
    if (oldCap > 0) {
        ...
    }

    //初始容量设置为阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;

    //table为null的情况
    //newcap赋值为DEFAULT_INITIAL_CAPACITY,
    //newthr赋值为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
    //DEFAULT_INITIAL_CAPACITY为默认初始容量16,定义:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //DEFAULT_LOAD_FACTOR为默认缓存大小0.75,定义:static final float DEFAULT_LOAD_FACTOR = 0.75f;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    //修改newThr
    if (newThr == 0) {
        ...
    }

    //将newthr赋值给threshold,即缓存大小设置完毕
    threshold = newThr;

    @SuppressWarnings({"rawtypes","unchecked"})
    //因为table为数组,因此扩容需要新建立数组
    //然后将新数组赋给table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    //如果旧数组table不为null,则需要将旧数组中的数据转移到新数组,具体先不研究
    if (oldTab != null) {
                . . .
    }
    
    //最后返回新的数组table
    return newTab;
}
    treeifyBin():判断对应链表是否需要树化
        先进行判断,如果数组长度小于MIN_TREEIFY_CAPACITY(默认64),则调用resize()进行扩容
        否则进行树化。
    
putVal()源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {

    //tab为table
    //n用来存放table长度;
    //p用来存放table中将要存放的位置的对象
    //i用来存放table中将要存放位置的下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //将table赋给tab,判断是否为null或者判断length是否为0
    //若table不为空,则跳过该段代码,否则进入resize()方法
    //将长度赋给n
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    //计算出待存放位置,即i位置的table[i]为null
    //1.将 n-1 和 传进来的hash 进行按位与,并将其赋值给 i
    //2.将tab[i],即按位与得到下标的位置在数组中的对象赋值给p
    //3.如果p为null,说明该位置还没有存放,则新建一个结点存放进去,结点信息在上面
    //4.然后该结点赋值给tab[i]
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //计算出待存放位置不为空,即 待存储数据的hash值 与 之前存储过的数据hash值 一致,
    //是否存入新数据如下:
    else {
        //创建辅助变量
        Node<K,V> e; K k;
        //如果 p.hash即数组当前位置的链表的第一个元素的hash值 与 hash即待插入的新元素的hash值相同
        //且满足下面条件之一:(将p.key赋给k)
        //    k == key 即 第一个元素的key 和 带加入元素的key 是同一个对象
        //    key != null && key.equals(k) 即 key非空 且 key 和 kequals相同,即内容相同
        //则将 p 赋值给 e,p就是当前数组当前位置的链表的第一个元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果p是一颗红黑树,就以红黑树添加元素的方式进行比较并添加新的对象
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果不满足上述两种情况,即值不同,且p还不是红黑树
        //则当前p为一个链表
        //就进行循环
        else {
            for (int binCount = 0; ; ++binCount) {
                //遍历到了尾部,追加新节点到尾部
                //先判断p.next是否为null
                //如果为null则直接将新结点存入,并退出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果新增加元素后 > TREEIFY_THRESHOLD(默认为8)-1=7,即有了8个结点,
                    //则调用treeifyBin()进行判断是否树化,方法在上面
                    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不等于null,则说明原来存在与带加入节点hash值相同,但是value值不同节点
        //然后用新的value代替旧的value
        //则返回旧value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //空方法,没实现,LinkedHashMap的时候有用到
            afterNodeAccess(e);
            return oldValue;
        }
    }

    //modCount即操作数+1
    ++modCount;

    //size即数组大小+1
    //+1之后的size如果大于了threshold,则需要扩容
    //注意,此处是size>threshold,而size每次增加是在加入一个结点之后;
    //意味着并不是数组table用到了threshold个,而是结点达到threshold个。
    if (++size > threshold)
        resize();

    //该函数在HashMap中是一个空函数
    //该函数由HashMap的子类实现,用来实现排序存放等。
    afterNodeInsertion(evict);
    
    //最后返回null为添加成功
    return null;
}

二、LinkedHashSet

  1. LinkedHashSet是HashSet的子类。
  2. LinkedHashSet底层是 LinKedHashMap,维护了一个 数组 + 双向链表
  3. LinkedHashSet根据元素的hashcode决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet不允许添加重复元素。

源码

  • LinkedHashSet维护一个hash表和双向链表
    • 该类有两个属性head 和 tail ,用来表示头节点和尾节点
  • 每一个节点还有 pre 和 next 属性,用来形成双向链表
  • 添加一个元素时,先求hash值,再求索引,确定其在table中的位置
    • 如果没有重复,跟HashSet一样加入,之后再加入双向链表
    • 有重复则不加入
  • 因此,遍历时使用链表,就会呈现和添加顺序一样的遍历顺序了。
  • 存放的节点不是Node了,而是
1、构造器:
    可见构造器是直接调用父类的有参构造器,默认初始容量为16,阈值为0.75。
    注意, HashSet()中创建的对象是LinkedHashMap(),而不是HashMap()。
    
    
2、add()添加元素:
    与HashSet类似,也是直接调用LinkedHashMap的add()方法
    然后调用 HashMap 的put()方法,因为LinkedHashMap并未重写该方法
    再调用 HashMap 的putVal()方法
    
    
putVal()方法:
    与HashSet一样,但是在 newNode()时,使用的是LinkedHaspMap的方法:
        先创建一个Entry对象。
        然后调用父类构造方法构造Node。
            注意:Entry继承了HashMap的内部类Node
        再调用linkNodeLast()方法将该节点链接在链表当中。
    
    
    
    与HashSet不同的还有在最后 afterNodeInsertion(evict)是实现了的
    
    

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

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

相关文章

四、.Net8对接Ollama实现文字翻译(.Net8+SemanticKernel+Ollama)本地运行自己的大模型

.Net8SemanticKernelOllama 一、Semantic Kernel官方定义SK能做什么&#xff1f; 二、基本使用1、普通对话2、使用插件实现文本翻译功能 三、IChatCompletionService、ITextGenerationService、ITextEmbeddingGenerationService 很多情况都有这样的需求&#xff0c;使用自有系统…

PS系统教学02

多个图片同时进行打开 在素材库里面选中两张图片&#xff0c;直接拖进PS软件中&#xff0c;此时会显示其中一张。当按下回车键会显示另一张。 当图层过多&#xff0c;需要进行选择&#xff0c;其中某一张图片&#xff0c;按住Ctrl键&#xff0c;进行选择点击&#xff0c;可以移…

Windows下切换不同版本的CUDA

在环境变量处将需要使用的CUDA版本的如图所框选的环境变量移到其他版本环境变量的前方即可 PS&#xff1a;改环境变量后重启命令行再查看版本~

【Jmeter】性能测试之压测脚本生成,也可以录制接口自动化测试场景

准备工作-10分中药录制HTTPS脚本&#xff0c;需配置证书 准备工作-10分中药 以https://www.baidu.com/这个地址为录制脚本的示例。 录制脚本前的准备工作当然是得先把Jmeter下载安装好、JDK环境配置好、打开Jmeter.bat&#xff0c;打开cmd&#xff0c;输入ipconfig&#xff0c;…

SpringMVC枚举类型字段处理

在日常的项目开发中经常会遇到一些取值范围固定的字段&#xff0c;例如性别、证件类型、会员等级等&#xff0c;此时我们可以利用枚举来最大程度减少字段的乱定义&#xff0c;统一管理枚举的值。 SpringMVC中对于枚举也有默认的处理策略&#xff1a; 对于RequestParam&#xf…

将金融文件转化为ASP.NET Core C#中的智能且安全的表单

在财务流程中&#xff0c;法律要求所有协议和合同均需有纸质副本。无论您使用的是移动数据采集前端、功能强大的文档查看器还是任何其他数据采集流程&#xff0c;最终结果都是经过数字签名的 PDF 或纸质副本。 在大流行期间&#xff0c;数字文档处理变得流行且必要。这些年来&…

高边坡监测常用的主要仪器设备

随着人类的发展&#xff0c;近几年由于人类生活生产的破坏&#xff0c;地球环境不断恶化。鉴于这一现象&#xff0c;监测行业也随之应运而生。常见的监测类型有&#xff1a;边坡监测&#xff0c;地灾监测&#xff0c;水库监测&#xff0c;大坝监测&#xff0c;矿山监测&#xf…

KineFX —— 简介

KineFX是绑定和动画的框架和工具集&#xff0c;可在SOP级别创建和编辑角色&#xff1b;可从头创建自己的KineFX角色&#xff0c;或使用特定的KineFX SOP和常规的SOP去编辑导入的角色和动画&#xff1b; 程序化绑定 KineFX构建与程序化绑定的原则上&#xff0c;可快速非破坏性迭…

Java基础入门day60

day60 购物车案例补充 设置欢迎页 打开也系统&#xff0c;就可以直接看到商品列表页面 之前曾经设置过欢迎页&#xff0c;都是针对页面&#xff0c;可以有html页面&#xff0c;也可以有jsp页面 但是今天我们将一个servlet设置成欢迎页 在web.xml文件中设置欢迎页 <welcome…

初探Flask:Pycharm社区版创建Flask项目

文章目录 一、创建工程二、引入Flask库1.使用命令行安装Flask2.在PyCharm中安装Flask 三、创建Flask框架基本文件四、运行项目 本文将带您一步步创建一个简单的Flask项目&#xff0c;包括设置Python环境、安装Flask库以及创建基本的项目结构。 一、创建工程 首先&#xff0c;确…

找到可靠的APP外包开发公司

找到可靠的APP外包开发公司需要经过一系列细致的筛选和评估。以下是寻找和选择一家合适的APP外包开发公司的步骤和注意事项。选择一家可靠的APP外包开发公司需要经过详细的研究和多方面的评估&#xff0c;确保公司能够满足项目需求并提供高质量的服务。北京木奇移动技术有限公司…

【乐吾乐3D可视化组态编辑器】灯光

灯光 在场景属性中&#xff0c;我们介绍了HDR&#xff0c;它的作用是为场景提供环境光&#xff0c;如果网格设置了PBR材质&#xff0c;那么网格表面就会反射出光照效果。这是为场景提供环境光的手段之一&#xff0c;但是它也有缺陷&#xff0c;一是只对PBR材质有效&#xff0c…

采用Java+ SpringBoot+ IntelliJ+idea开发的ADR药物不良反应监测系统源码

采用Java SpringBoot IntelliJidea开发的ADR药物不良反应监测系统源码 ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统具有广泛的应用场景&#xff0c;以下是一些主要的应用场景&#xff1a…

MT3048 区间按位或

思路&#xff1a; 使用ST表。ST表求区间最大值改为按位或即可。 ST模板可参考MT3024 maxmin 代码&#xff1a; 1.暴力6/10 #include <bits/stdc.h> using namespace std; const int N 2e5 10; const int M 5e5 10; int n, m; int num[N]; int main() {cin >&…

前端实习记录——git篇(公司拉取项目流程)

实习中第一步就是拉取项目&#xff0c;看项目代码&#xff0c;下面总结一下我在公司项目拉取项目流程。 1、联系leader开通gitlab账号 2、查看/配置git用户名和密码 &#xff08;1&#xff09;查看 git config user.name git config user.email git config user.password &…

8086 汇编笔记(二):寄存器(内存访问)

一、内存中字的存储 字单元的概念&#xff1a;字单元&#xff0c;即存放一个字型数据(16 位)的内存单元&#xff0c;由两个地址连续的内存单元组成 由上一章学习可知&#xff1a;高地址内存单元中存放字型数据的高位字节&#xff0c;低地址内存单元中存放字型数据的低位字节 …

【喜报】科大睿智多家服务企业上榜2024年第四批DCMM名单

近日&#xff0c;DCMM官方平台发布通知公告&#xff0c;根据《数据管理能力成熟度评估工作管理办法(暂行)》的有关规定&#xff0c;经单位自愿申请&#xff0c;评估机构评估、专家评审及公示&#xff0c;下列27单位获得数据管理能力成熟度等级证书。小编祝贺多家服务企业上榜20…

四川古力未来科技抖音小店畅享多重好处

在当今数字化浪潮席卷之下&#xff0c;四川古力未来科技抖音小店以其独特的魅力&#xff0c;正逐渐成为消费者们的新宠。作为融合了先进科技与便捷购物体验的创新平台&#xff0c;它不仅能够满足消费者的多样化需求&#xff0c;更在提升购物体验、优化服务流程等方面展现出了显…

【ubuntu20】--- 定时同步文件

在编程的艺术世界里&#xff0c;代码和灵感需要寻找到最佳的交融点&#xff0c;才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里&#xff0c;我们将共同追寻这种完美结合&#xff0c;为未来的世界留下属于我们的独特印记。 【Linux命令】--- 多核压缩命令大全&…

openresty(Nginx) 隐藏 软件包名称 版本号 升级新版本

1 访问错误或者异常的URL 2 修改配置&#xff0c;重新编译&#xff0c;升级 #修改版本等 vim ./bundle/nginx-1.13.6/src/core/nginx.h #define nginx_version 1013006 #define NGINX_VERSION "1.13.6" #define NGINX_VER "openresty/&q…