Java集合(下)

news2024/11/16 15:33:31

Map(重要)

HashMap和Hashtable的区别

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。
  • 哈希函数的实现HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的 hashCode() 值。

HashMap和HashSet区别

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMapHashSet
实现了 Map 接口实现 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

HashMap和TreeMap区别

TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

TreeMap 继承关系图TreeMap 继承关系图

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

NavigableMap 接口提供了丰富的方法来探索和操作键值对:

  1. 定向搜索: ceilingEntry(), floorEntry(), higherEntry()lowerEntry() 等方法可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。
  2. 子集操作: subMap(), headMap()tailMap() 方法可以高效地创建原集合的子集视图,而无需复制整个集合。
  3. 逆序视图:descendingMap() 方法返回一个逆序的 NavigableMap 视图,使得可以反向迭代整个 TreeMap
  4. 边界操作: firstEntry(), lastEntry(), pollFirstEntry()pollLastEntry() 等方法可以方便地访问和移除元素。

这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 TreeMap 成为了处理有序集合搜索问题的强大工具。

综上,相比于HashMap来说, TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet如何检查重复?

在 JDK1.8 中,HashSetadd()方法只是简单的调用了HashMapput()方法,并且判断了一下返回值以确保是否有重复元素。

也就是说,在 JDK1.8 中,实际上无论HashSet中是否已经存在了某元素,HashSet都会直接插入,只是会在add()方法的返回值处告诉我们插入前是否存在相同元素

HashMap底层实现

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

JDK1.8 之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap的长度为什么是2的幂次方

简单总结一下 HashMap 的长度是 2 的幂次方的原因:

  1. 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,hash % length 等价于 hash & (length - 1)
  2. 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
  3. 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。

HashMap多线程操作导致死循环问题

JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap

HashMap为什么线程不安全?

JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。

数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。

JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMapput 操作会导致线程不安全,具体来说会有数据覆盖的风险。

举个例子:

  • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
  • 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
  • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。

HashMap常见的遍历方式?

图片

HashMap 遍历从大的方向来说,可分为以下 4 类

  1. 迭代器(Iterator)方式遍历;
  2. For Each 方式遍历;
  3. Lambda 表达式遍历(JDK 1.8+);
  4. Streams API 遍历(JDK 1.8+)。

但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

  1. 使用迭代器(Iterator)EntrySet 的方式进行遍历;
  2. 使用迭代器(Iterator)KeySet 的方式进行遍历;
  3. 使用 For Each EntrySet 的方式进行遍历;
  4. 使用 For Each KeySet 的方式进行遍历;
  5. 使用 Lambda 表达式的方式进行遍历;
  6. 使用 Streams API 单线程的方式进行遍历;
  7. 使用 Streams API 多线程的方式进行遍历。

ConcurrentHashMap和Hashtable的区别

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

  • 实现线程安全的方式(重要):

    • 在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

    • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

Hashtable

Hashtable 的内部结构

jdk1.7的ConcurrentHashMap

Java7 ConcurrentHashMap 存储结构

jdk1.8的ConcurrentHashMap

Java8 ConcurrentHashMap 存储结构

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.8 之前

Java7 ConcurrentHashMap 存储结构Java7 ConcurrentHashMap 存储结构

首先将数据分为一段一段(这个“段”就是 Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

JDK1.8 之后

Java8 ConcurrentHashMap 存储结构Java8 ConcurrentHashMap 存储结构

Java 8 几乎完全重写了 ConcurrentHashMap,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

Java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式:JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

ConcurrentHashMap 为什么 key 和 value 不能为 null?

ConcurrentHashMap 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 ConcurrentHashMap 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 ConcurrentHashMap 中的,还是因为找不到对应的键而返回的。

拿 get 方法取值来说,返回的结果为 null 存在两种情况:

  • 值没有在集合中 ;
  • 值本身就是 null。

这也就是二义性的由来。

多线程环境下,存在一个线程操作该 ConcurrentHashMap 时,其他的线程将该 ConcurrentHashMap 修改的情况,所以无法通过 containsKey(key) 来判断否存在这个键值对,也就没办法解决二义性问题了。

与此形成对比的是,HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 HashMap 修改的情况,所以可以通过 contains(key)来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。

也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。

如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。

ConcurrentHashMap 能保证复合操作的原子性吗?

ConcurrentHashMap 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 HashMap 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!

复合操作是指由多个基本操作(如putgetremovecontainsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。

那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。

那如何保证 ConcurrentHashMap 复合操作的原子性呢?

ConcurrentHashMap 提供了一些原子性的复合操作,如 putIfAbsentcomputecomputeIfAbsentcomputeIfPresentmerge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。

Collections工具类(不重要)

Collections 工具类常用方法:

  • 排序
  • 查找,替换操作
  • 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

同步控制

Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

我们知道 HashSetTreeSetArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections 提供了多个静态方法可以把他们包装成线程同步的集合。

最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。

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

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

相关文章

也遇到过 PIL Image “image file is truncated“的问题

背景前言 属于活久见系列,最近工作上遇了该问题! 背景:前端 APP使用 Android CameraX 的接口,拍摄并上传图片,然后 Python后端服务对图片裁剪与压缩处理。后端服务处理图片时有遇到image file is truncated的情况。还…

Spring AOP实现原理-动态代理

目录 代理的基础概念 示例1:静态代理(场景:客户通过中介租房东的房子) 示例2:JDK动态代理实现房东、中介出租房屋 示例3:CGLib动态代理实现房东出租房屋 示例4:观察Spring IOC容器中代理对象…

One-Class Classification: A Survey

I. INTRODUCTION 1.定义 OCC 是一种特殊的多类分类,训练数据仅来自单个正类。目标是学习表示和/或分类器,以便在推理过程中识别正类查询。 2.应用 异常图像检测、异常事件检测、生物识别(活体检测、反诈骗) 3.与其他领域的比…

Python连接Kafka收发数据等操作

目录 一、Kafka 二、发送端(生产者) 三、接收端(消费者) 四、其他操作 一、Kafka Apache Kafka 是一个开源流处理平台,由 LinkedIn 开发,并于 2011 年成为 Apache 软件基金会的一部分。Kafka 广泛用于构…

在Java中,关于final、static关键字与方法的重写和继承【易错点】

在Java中,关于final、static关键字与方法的重写和继承【易错点】 1.final方法不能被重写2.static方法不是重写,而是遮蔽3.final与static的组合4.final与继承5.static与继承 1.final方法不能被重写 如果父类中的方法被声明为final,那么这个方法…

开源音频处理项目推荐【持续更新】

Audacity 介绍:Audacity是一款功能强大的开源音频编辑软件,适用于多种操作系统,包括Windows、macOS和Linux。它支持多轨音频编辑、录制,并且提供了丰富的音频处理功能,如剪切、复制、粘贴、混音、降噪等 。Audacity的…

基于Python+flask+MySQL+HTML的全国范围水质分析预测系统,可视化用echarts,预测算法随机森林

1绪论 近年来,水质监测系统的进步显著,这在全球环保意识不断提升的背景下尤为明显。大量资源被投入到水质监测技术的研发和应用中,以不断优化监测效能。水资源的保护及健康环境的维护,这种趋势旨在提升人们生活质量,确…

微软宣称其新工具可纠正人工智能幻觉 但专家依然对此表示怀疑

人工智能经常胡言乱语,微软现在说它有办法解决这个问题,但我们有理由对此持怀疑态度。微软今天发布了一项名为"更正"(Correction)的服务,它可以自动修改人工智能生成的与事实不符的文本。Correction 首先会标…

华为认证HCIA篇--网络通信基础

大家好呀!我是reload。今天来带大家学习一下华为认证ia篇的网络通信基础部分,偏重一些基础的认识和概念性的东西。如果对网络通信熟悉的小伙伴可以选择跳过,如果是新手或小白的话建议还是看一看,先有个印象,好为后续的…

8.隐私与安全 - 使用ChatGPT时的注意事项【8/10】

引言 在数字时代,隐私和安全已成为全球关注的焦点。随着技术的发展,个人信息和数据的收集、存储、处理和传输变得越来越普遍,这既带来了便利,也带来了风险。保护个人隐私和数据安全不仅是法律的要求,也是维护公众信任…

solidwork中查看装配体螺丝或零件

假设我的PETG打印件到了,想知道这个螺丝的型号,怎么办 解决办法: 第一步先看看有没有固定的字样 如果固定的话是不行的。需要这样做: 把这里给关了 接下来第二步,点击你想查看的螺丝 然后就会跳到零件图 可以看到直径…

Cloudflare为网站添加AI审计 可检查AI爬虫何时抓取和抓取频次以及直接屏蔽爬虫

网络服务提供商 Cloudflare 宣布即日起为所有网站 (包括免费托管的网站) 带来 AI 审计功能,该功能目前处于测试阶段,可以分析 AI 公司的爬虫和抓爬数据。新的 AI 审计工具 (Cloudflare AI Audit) 主要提供 AI 公司的爬虫何时到网站来抓取数据、抓取的数据…

Unity 热更新(HybridCLR+Addressable)-资源更新

七、资源更新 创建一个叫Aot的文件夹,用来存放不会热更新的资源 这个修改为第三个 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b8be5e6465184ad5ad6173c6870bfa06.png 这个是更新 在更新或者打包时遇到端口被占的报错,不用理会&#xf…

二、认识大模型

认识大模型 什么是大模型?发展趋势AGI是不是泡沫大模型对比【时效】大模型特点大模型技术原理向量化除了向量化,大模型还具有特征提取特点 总结结语 什么是大模型? 大模型是大规模语言模型(Large Language Model)的简…

mysql如何替换数据库所有表中某些字段含有的特定值

目录 背景查询所有表名查询表的所有字段过虑特征字段替换字段中含有的特定值 背景 公司的测试域名更换了,导致存放在数据库中的域名也要跟着替换,当然把域名存放在数据库表中是不科学的,不建议这样做,但公司的同事就这样做了&…

由动静压之比求马赫数的MATLAB函数

函数介绍 输入:动静压之比 p r e pre pre 输出:马赫数 M a c h Mach Mach 【注】仅适合亚音速的情况,如果动静压之比过大或过小,会有相应的提示 函数源代码 function [m] pre2mach(pre) m(5*(pre1).^0.2857-5).^0.5; if pre&l…

Leetcode 螺旋矩阵

算法思想: 这个算法的目标是按照顺时针螺旋的顺序从矩阵中取出元素。为了做到这一点,整个思路可以分成几个关键步骤: 定义边界:首先需要定义四个边界变量: left:当前左边界的索引。right:当前右…

uniapp 实现3d轮播图,也就是中间的放大两边的缩小 用swiper和swiper-item就能实现

话不多说&#xff0c;直接上代码&#xff0c;无需引入外部资源&#xff0c; 用swiper和swiper-item就能实现 先上结构代码 <swiper class"header" circular previous-margin"80rpx" next-margin"60rpx" :current"current"change&…

点亮城市安全:高科技助力精准定位路灯漏电‘隐形杀手

在城市的每一个角落&#xff0c;路灯如同守夜人&#xff0c;默默照亮归家的路。然而&#xff0c;当这些守护者出现“漏电”隐患时&#xff0c;不仅威胁着行人的安全&#xff0c;还可能引发一系列电气故障。那么&#xff0c;如何精准快速地找出这些隐藏的漏电点&#xff0c;并有…

二叉树进阶oj题【二叉树相关10道oj题的解析和代码实现】

目录 二叉树进阶oj题1.根据二叉树创建字符串2.二叉树的层序遍历3.二叉树的层序遍历 II4.二叉树的最近公共祖先5.二叉搜索树和双向链表6.从前序与中序遍历序列构造二叉树7.从中序和后序遍历序列来构造二叉树8.二叉树的前序遍历&#xff0c;非递归迭代实现9.二叉树中序遍历 &…