Java小白一文讲清Java中集合相关的知识点(五)

news/2024/9/18 9:18:03/ 标签: java, 开发语言

Set接口和常用方法

基本介绍

  • 无序(添加和取出的顺序不一样),没有索引
  • 不允许重复元素,所以最多包含一个null
  • JDK API 中Set接口的实现类有:
java">    public static void main(String[] args) {//1.以set接口的实现类HashSet来讲解Set 接口的方法//2.set接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null//3.set接口对象存放数据是无序的--即添加的顺序和取出的顺序不一致//4.注意:取出的顺序虽然不是添加的顺序,但是其是固定的,即放入和取出只会打乱一次,//  并不是说同样的一组数据,取得第二次和第三次顺序不一样;Set set = new HashSet();set.add("bingo!!");set.add("冲冲冲!!");set.add("kerwin");set.add("enjoy");set.add(true);set.add(null);set.add(null);System.out.println(set);//输出    [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]for (int i = 0; i < 5; i++) {System.out.println(set);}//输出结果如下/*** [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]* [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]* [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]* [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]* [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]* [null, bingo!!, 冲冲冲!!, enjoy, kerwin, true]*/}

常用方法

和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collecton接口一样

Set接口遍历方式(2种)

同Collection的遍历方式一样,因为Set接口是Collection接口的子接口

  • 可以使用迭代器
  • 增强for
  • 不能使用索引的方式来获取
java">@SuppressWarnings({"all"})
public class Journey {public static void main(String[] args) {Set set = new HashSet();set.add("bingo!!");set.add("冲冲冲!!");set.add("kerwin");set.add("enjoy");set.add(true);set.add(null);set.add(null);//遍历--使用迭代器System.out.println("================迭代器遍历===============");Iterator iterator = set.iterator();//此时要联想到迭代器的结构while (iterator.hasNext()) {//判断还有没有下个元素Object obj = iterator.next();System.out.println(obj);}//输出结果如下/*** null* bingo!!* 冲冲冲!!* enjoy* kerwin* true*///遍历--增强forSystem.out.println("=========增强for遍历===========");for (Object o : set){System.out.println("for->"+o);}//输出如下/*** for->null* for->bingo!!* for->冲冲冲!!* for->enjoy* for->kerwin* for->true*///set接口对象,不能通过索引来获取,所以无法用normal-for来循环获取}
}

HashSet的全面说明

  • HashSet实现了Set接口
  • HashSet实际上是HashMap 源码如下:
java">    public HashSet() {map = new HashMap<>();}
  • HashSet可以存放null值,但是只能有一个null
  • HashSet不保证元素是有序的,取决于Hash后,再确定索引的结果
  • 不能有重复元素/对象;
java">import java.util.*;@SuppressWarnings({"all"})
public class Journey {public static void main(String[] args) {Set hashSet = new HashSet();hashSet.add(true);hashSet.add(null);hashSet.add(null);System.out.println("存放了两个null,此时的hashSet为"+hashSet);//存放了两个null,此时的hashSet为[null, true]hashSet.add("bingo!!");hashSet.add("冲冲冲!!");hashSet.add("kerwin");hashSet.add("enjoy");System.out.println("不保证存入顺序和取出顺序一致哈");System.out.println("此时的hashset"+hashSet);//此时的hashset[null, bingo!!, 冲冲冲!!, enjoy, true, kerwin]//顺便一提,在执行add方法之后,会返回一个boolean值//如果添加成功,return true,else return falseSystem.out.println(hashSet.add("fulture"));//trueSystem.out.println(hashSet.add("kerwin"));//falsehashSet = new HashSet();//此时是重置了hashSet引用的指向,让其指向一个新的空HashSet对象System.out.println(hashSet);//[]hashSet.add("lucky");hashSet.add("lucky");//不能加进去hashSet.add(new Dog("star"));//能加进去hashSet.add(new Dog("star"));//能加进去System.out.println("此时的hashset="+hashSet);//此时的hashset=[lucky, star, star]//注意,此时的两个star是同名的不一样的两只狗哈,//再加深下理解,一道经典的面试题hashSet.add(new String("K"));//能加进去hashSet.add(new String("K"));//不能加进去//之后,看源码,分析,就可以知晓缘由了System.out.println(hashSet);//[lucky, star, star, K]   只有一个K}
}
@SuppressWarnings({"all"})
class Dog{//定义了一个Dog类private String name;public Dog(String name) {this.name = name;}@Overridepublic String toString() {return name;}
}

HashSet底层机制说明

说明:HashSet底层是HashMap,HashMap底层是数组+链表+红黑树

java">@SuppressWarnings({"all"})
public class Journey {public static void main(String[] args) {//模拟一个HashSet的底层(HashMap的底层结构)//1.创建一个数组,数组的类型是Node[]//2.有些人,直接把Node[] 数组称为表Node[] table = new Node[16];System.out.println("table = "+table);//3.创建结点Node john = new Node("john", null);table[2]= john;Node jack = new Node("jack",null);john.next = jack;//将jack结点挂载到john 上Node rose = new Node("Rose",null);jack.next = rose;//将rose结点挂载到jack上Node lucy = new Node("lucy", null);table[3] = lucy;System.out.println(table);}
}
class Node{//结点,存储数据,可以指向下一个结点,从而形成链表Object item;//存放数据Node next;//指向下一个结点public Node(Object item, Node next) {this.item = item;this.next = next;}
}

此时的table内部结构如图所示

在这里插入图片描述

HashSet扩容机制剖析

  • HashSet底层是HashMap
  • 添加一个元素时,先得到hash值,会转化成索引值
  • 找到存储数据表table,看这个索引位置是否已经存放了元素,如果没有,直接加入,如果有,则调用equals比较【这个equals程序员是可以自己重写的】,如果相同,就放弃添加,如果不相同,则添加到最后;
    • 比如经过hash函数,kerwin字符串放到了2号索引处,再添加一个kerwin字符串,经过hash函数计算,还是会映射到2号索引处,那么此时调用equals比较,得出相同,则放弃添加,反之,第二次如果加的是个luck的字符串,同样被映射到了2号索引处,那么,此时再比较,发现不一样,则会将luck挂载到kerwin字符串的后面;
  • 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
java">//以下剖析加进去一个元素“java”的整个流程public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add("java");hashSet.add("php");hashSet.add("java");System.out.println("set=" + hashSet);//set=[java, php]}
//对HashSet的add方法的源码剖析,先加第一个字符串java//先进入add方法内部,发现,执行put方法,继续进去public boolean add(E e) {//E 是泛型,之后会讲的//此中的key始终是随着你数据的添加再变化的,比如第一个事Java,第二个是phpreturn map.put(e, PRESENT)==null;   //PRESENT=(static) new Object(); 是可共享的哈//最后最后看//返回了个null,则二者一对比,返回true,说明可以塞元素进去,反之,如果不是null,则false//说明你添加的这个元素已经存在了  整个流程看完,java才加进去}//这是深挖PRESENT源码发现其是一个新创建的Object对象  作为valueprivate static final Object PRESENT = new Object();//进入put方法,发现其会先调用hash(),进去,之后再看下putVal方法
//	此中的value是add方法中创建,初始化的一个带有final static 的对象
//	其起初没有什么意义,用来占位,就是先统一在HashSet的value部分用这个PRESENT
//	来占位,即不管add多少次,key变化,但value始终是这个PRESENTpublic V put(K key, V value) {//此时key="java" value=PRESENTreturn putVal(hash(key), key, value, false, true);//这句最后也返回null给add()}//进入hash()方法内部,可以看到它是怎么hash的//key不为空,则先得到key的hash值h,然后将其算术右移16位;//这样做的目的是为了让不同的key尽量得到不同的哈希值,避免哈希碰撞//hash值不是hashCode哈,是hashCode经过处理后的
//	 通过hash(key),我们就得到了hash值,但其不等价于hashCode
//	 还需要经过h= key.hashCode()^(h>>>16) 经过hashCode,再按位异或,将h算术右移16位
/**
假设 key.hashCode() 返回 -1299402055。
h >>> 16 是将 h 无符号右移 16 位。
计算结果为:49950。
计算按位异或 h ^ (h >>> 16):
-1299402055 ^ 49950 = -1299451681
返回结果:
hashValue = -1299451681
*/static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//接下来就到了源码的核心部分putVal了//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语句表示如果当前table是null,或者大小=0,则说明是第一次扩容,到16if ((tab = table) == null || (n = tab.length) == 0)//table 就是hashMap的一个数组,类型是Node[],起初为null,于是进入resize()//将resize()的结果交给tab,然后取长度length,赋给n//可以往下跳着看resize()实现,其最后就会返回一个16n = (tab = resize()).length;//(1)这个if是根据你上面的key接下来计算其对应的hash值进而来计算该key应该存放在//	 table表的哪一个索引位置,并把这个位置的对象,赋给辅助变量p//(2)判断p是否为null//	2.1如果p 为null,表示还没有存放过元素,就创建一个Node,把key,value一起//	   放进去,key是你当前存放的哪个元素,value就是之前提到的PRESENT//		Node(key="java", value=PRESENT)//  2.2然后就放到该位置tab[i]   = newNode(hash, key, value, null);//此时返回去,查看变量tab空间情况,就会发现已经有个位置被放进去了元素"java"//可以看到其hash值比如3254803,其value {Object@550},还有next为nullif ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//底下这个分支就不会进去了else {Node<K,V> e; K k;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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}//修改次数++++modCount;//判断当前大小是否超过12,if yes //再进入resize扩容,此时只加了一个元素,不用扩容if (++size > threshold)resize();//底下这个方法是hashMap留给它的子类,eg LinkedHashMap去实现,再做些其他动作的//而对于HashMap而言,其就是个空方法,原因在于让子类去重新实现它,比如形成个有序链表啊,//挂在一个双向链表啊啥的 而设置的,对于HashMap,这个方法可以忽略afterNodeInsertion(evict);//接下来就返回个null,代表成功了,返回给put方法,其也返回个null//如果不返回null,则return 其旧的值oldValue,可以往上瞅瞅return null;}//进入resize(),final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//把起初的table赋给了oldTab//起初为null,所以oldCap为0,然后往下走,进入if-else的第三个分支//使得newCap为默认的初始化容量int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 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}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaults//第一次是跳到了这儿,在这儿深挖下DEFAULT_INITIAL_CAPACITY/**将1左移4位,也就是连乘以4个2,即16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16*/newCap = DEFAULT_INITIAL_CAPACITY;//初始化了容量为16后,接下来又计算了一个临界值threshold(阈值)//这里的临界值就是放元素到了什么程度会考虑扩容的一条划定线//假设有16个空间,但hashSet是个未雨绸缪的人,它在容量到了75%时就会扩容//即当放的元素到了16*0.75=12个时,它就扩容了//它担心,余下的4个万一放不下之后万一一次性放进来的多个元素造成阻塞,so do it//就是一个教室,50个座位,已经陆陆续续报到了45个人,那么班主任就会赶紧扩容//二者的本质有点相似newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}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"})//底下这句话是关键,执行完后,newTab就有了16个空间Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//newCap=16//接下来就把newTab赋给了tabletable = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> 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 { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}}}}}//最后把newTab给返回,使得n=16return newTab;}

java_453">附上此时数组中加进去了第一个元素"java"字符串后的空间情况

在这里插入图片描述

javaphpjava_456"># 接下来,已经加进去了第一个元素“java”的基础上,下一篇文章接着剖析,加进去第二个元素“php”和第三个元素“java”的整个流程,更加复杂,挺住哈!!!


http://www.ppmy.cn/news/1525138.html

相关文章

手机玩机常识-------诺基亚系列机型3/5/6/7/8详细的刷机教程步骤 手机参考救砖刷机教程

诺基亚手机 诺基亚&#xff08;Nokia Corporation&#xff09;&#xff0c;成立于1865年&#xff0c;是一家主要从事移动通信设备生产和相关服务的手机公司 &#xff0c;总部位于芬兰埃斯波 。从1996年开始&#xff0c;诺基亚手机连续15年占据手机市场份额第一位置&…

【测试】——自动化测试入门(Selenium环境搭建)

&#x1f4d6; 前言&#xff1a;本文介绍了自动化测试的基础知识&#xff0c;重点讲解了Selenium环境的搭建。内容包括自动化测试的定义、自动化测试金字塔模型、Selenium的特点和工作原理&#xff0c;以及如何在Java环境中配置和使用Selenium进行UI自动化测试。 目录 &#x1…

性能测试经典案例解析——政务查询系统

各位好&#xff0c;我是 道普云 一站式云测试SaaS平台。一个在软件测试道路上不断折腾十余年的萌新。 欢迎关注我的主页 道普云 文章内容具有一定门槛&#xff0c;建议先赞再收藏慢慢学习&#xff0c;有不懂的问题欢迎私聊我。 希望这篇文章对想提高软件测试水平的你有所帮…

go 语言常见问题(4)

31. go语言编程的好处是什么 编译和运行都很快。在语言层级支持并行操作。有垃圾处理器。内置字符串和 maps。函数是 go 语言的最基本编程单位。 32. 说说go语言的select机制 select 机制用来处理异步 IO 问题select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操…

【C语言】归并排序递归和非递归——动图演示

目录 一、归并排序思想1.1 基本思想1.2 大体思路 二、实现归并排序&#xff08;递归&#xff09;三、实现归并排序&#xff08;非递归&#xff09;3.1 实现思路&#xff1a;3.2 越界处理3.3 时间复杂度和空间复杂度 总结 一、归并排序思想 1.1 基本思想 归并排序&#xff08;M…

redis为什么快

春内存访问&#xff0c;相比数据库访问磁盘要快单线程&#xff0c;避免上下文切换带来的cpu开销渐进式Rehash。减少阻塞网络模型多路复用&#xff0c;reactor模型 常用基本数据类型 5个基本数据类型2个高级数据结构&#xff08;bitmaps、hyperlog&#xff09; redis高级功能…

Gitea Action注册runner

我的是gitea也可以和github 兼容&#xff0c;只是没有github 那么靓而已 安装一个gitea仓库 docker run -d --name gitea \-p3000:3000 -p2222:22 \-v /git/data:/data \ -v /etc/timezone:/etc/timezone:ro \-v /etc/localtime:/etc/localtime:ro \gitea/gitea:1.21.1setti…

【漏洞复现】某4国语言抖音点赞系统存在任意文件上传漏洞

漏洞描述 某4国语言 中文+英文+泰语+繁体 UI也非常不错 功能比较完善!【系统功能】1.任务后台添加/用户发布,后台审核 2.机器人、大转盘;已完善 3.支付可以对接第三方和线下银行卡收款;4.后台增加员工账号(推广员专属账号),可以查看员工推广报表;5.会员等级功能,会员级…

wireshark打开时空白|没有接口,卸载重装可以解决

解决方法&#xff1a;卸载wireshark,全选卸载干净&#xff0c;重新安装旧版的wireshark4.2.7, 甚至cmd下运行net start npf时显示服务名无效&#xff0c;但打开wireshark仍有多个接口 错误描述&#xff1a; 一开始下载的是wireshark的最新版&#xff0c;win11 x64 在安装wir…

Redis Sentinel(哨兵)详解

目录 一&#xff1a;什么是Sentinel&#xff08;哨兵&#xff09; 二&#xff1a;Sentinel有什么用 1.监控 2.故障转移 3通知 4.配置提供 三&#xff1a;Sentinel如何检测master节点宕机 1.主观下线 2.客观下线 四&#xff1a;Sentinel是如何选举出新的master 1.s…

学习常用的Docker命令

Docker作为一种强大的容器化技术&#xff0c;为开发者提供了便捷的应用部署和管理方式。本文将介绍Docker常用命令&#xff0c;按照不同的操作分类&#xff0c;旨在帮助初学者更好地理解和使用Docker。Docker 常用命令可以分为以下几类&#xff1a; 容器命令&#xff1a;主要用…

Qt常用控件——QTextEdit

文章目录 QTextEdit核心属性和信号同步显示示例信号示例 QTextEdit核心属性和信号 QTextEdit表示多行输入框&#xff0c;是一个富文本和markdown编辑器&#xff0c;并且能在内存超出编辑框范围时自动提供滚动条。 QPlainTexEdit是纯文本&#xff0c;QTextEdit不仅表示纯文本&a…

21. 合并两个有序链表【 力扣(LeetCode) 】

一、题目描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 二、测试用例 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 []…

java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)

项目简介 智能无人仓库管理实现了以下功能&#xff1a; 基于Spring Boot智能无人仓库管理的主要使用者分为&#xff1a; 管理员的功能有&#xff1a;员工信息的查询管理&#xff0c;可以删除员工信息、修改员工信息、新增员工信息 &#x1f495;&#x1f495;作者&#xff1a…

MySQL 大量 IN 的查询优化

背景 &#xff08;1&#xff09;MySQL 8.0 版本 &#xff08;2&#xff09;业务中遇到大量 IN 的查询&#xff0c;例&#xff1a; SELECT id, username, icon FROM users WHERE id IN (123, 523, 1343, ...);其中 id 为主键&#xff0c;IN 的列表长度有 8000 多个 问题 …

Java数据结构应用(力扣题20. 有效的括号)

给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括…

深入解析 `node-html-to-image` 库及其配置选项

深入解析 node-html-to-image 库及其配置选项 node-html-to-image 是一个功能强大的 Node.js 库&#xff0c;它可以将 HTML 内容转换为图像。该库利用 Puppeteer&#xff08;一个无头 Chrome 浏览器&#xff09;来渲染 HTML 并生成图像。本文将详细介绍 node-html-to-image 库…

如何在Oracle中实现数据的加密

在Oracle数据库中实现数据加密是一项重要的安全措施&#xff0c;它可以保护存储在数据库中的敏感信息不被未授权访问。Oracle提供了多种数据加密方法&#xff0c;包括透明数据加密&#xff08;TDE&#xff09;、列级加密和使用内置加密函数等。以下是一些在Oracle中实现数据加密…

SQL编程题复习(24/9/13)

练习题 x40 10-193 在顾客表(customers)中找出所在城市(City)为London的公司名(CompanyName)和联系人名(ContactName)10-194 查询价格低于1600美元的个人计算机的型号、速度及硬盘容量,将"speed"改为"兆赫"&#xff0c;"hd"改为"吉字节&quo…

Excel文档的读取(3)

我们继续观察“销售订单数据”这张工作表。这张表里的每一行其实就是一个订单。下一步&#xff0c;我们需要在工作表里&#xff0c;逐行去判断哪些订单商品是“火龙果可乐”&#xff0c;并把对应的订单总价添加到当月售卖总金额里。此处&#xff0c;我们需要用到行数据的遍历。…