首页 > 编程语言 >Java集合框架

Java集合框架

时间:2024-08-15 20:52:04浏览次数:15  
标签:Java HashMap 框架 元素 数组 链表 线程 哈希 集合

常见的集合框架

Java集合框架可以分为两大的支线:

①、Collection,主要由List、Set、Queue组成:

  • List代表有序、可重复的集合,典型代表就是封装了动态数组的ArrayList和封装了链表的LinkedList
  • Set代表无序、不可重复的集合,典型代表就是HashSet和TreeSet;
  • Queue代表队列,典型代表就是双端队列ArrayDeque,以及优先级队列PriorityQueue。

②、Map,代表键值对的集合,典型代表就是HashMap。

List

ArrayList和LinkedList区别

ArrayList 和 LinkedList 的区别主要体现在数据结构、用途、是否支持随机访问、内存占用等方面。

**数据结构:**ArrayList是基于数组实现,LinkedList基于链表实现

**用途:**多数情况下,ArrayList更利于查找,LinkedList更利于增删

**是否支持随机访问:**①、ArrayList是基于数组的,也实现了RandomAccess接口,所以它支持随机访问,可以通过下标直接获取元素。
②、LinkedList是基于链表的,所以它没法根据下标直接获取元素,不支持随机访问,所以它也没有实现RandomAccess接口。

**内存占用:**ArrayList是基于数组的,是一块连续的内存空间,所以它的内存占用是比较紧凑的;但如果涉及到扩容,就会重新分配内存,空间是原来的1.5倍,存在一定的空间浪费。
LinkedList是基于链表的,每个节点都有一个指向下一个节点和上一个节点的引用,于是每个节点占用的内存空间稍微大一点。

使用场景不同:

ArrayList 适用于:

  • 随机访问频繁:需要频繁通过索引访问元素的场景。
  • 读取操作远多于写入操作:如存储不经常改变的列表。
  • 末尾添加元素:需要频繁在列表末尾添加元素的场景。

LinkedList 适用于:

  • 频繁插入和删除:在列表中间频繁插入和删除元素的场景。
  • 不需要快速随机访问:顺序访问多于随机访问的场景。
  • 队列和栈:由于其双向链表的特性,LinkedList 可以高效地实现队列(FIFO)和栈(LIFO)。
ArrayList 的扩容机制

ArrayList 确切地说,应该叫做动态数组,因为它的底层是通过数组来实现的,当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。

扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。

ArrayList 怎么序列化的?

ArrayList 的序列化不太一样,它使用transient修饰存储元素的elementData的数组,transient关键字的作用是让被修饰的成员属性不被序列化。

为什么最 ArrayList 不直接序列化元素数组呢?

出于效率的考虑,数组可能长度 100,但实际只用了 50,剩下的 50 其实不用序列化,这样可以提高序列化和反序列化的效率,还可以节省内存空间。

那 ArrayList 怎么序列化呢?

ArrayList 通过两个方法readObject、writeObject自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStreamObjectInputStream来进行序列化和反序列化。

有哪几种实现 ArrayList 线程安全的方法?

可以使用 Collections.synchronizedList() 方法,它将返回一个线程安全的 List。

SynchronizedList list = Collections.synchronizedList(new ArrayList());

内部是通过 synchronized 关键字加锁来实现的。

也可以直接使用CopyOnWriteArrayList,它是线程安全的,遵循写时复制的原则,每当对列表进行修改(例如添加、删除或更改元素)时,都会创建列表的一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然可以继续。

CopyOnWriteArrayList list = new CopyOnWriteArrayList();

通俗的讲,CopyOnWrite 就是当我们往一个容器添加元素的时候,不直接往容器中添加,而是先复制出一个新的容器,然后在新的容器里添加元素,添加完之后,再将原容器的引用指向新的容器。多个线程在读的时候,不需要加锁,因为当前容器不会添加任何元素。这样就实现了线程安全。

CopyOnWriteArrayList 了解多少?

CopyOnWriteArrayList 就是线程安全版本的 ArrayList。

它的名字叫CopyOnWrite——写时复制,已经明示了它的原理。

CopyOnWriteArrayList 采用了一种读写分离的并发策略。CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

Set

HashSet 的底层实现

HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。

实际开发中,HashSet 并不常用,比如,如果我们需要按照顺序存储一组元素,那么 ArrayList 和 LinkedList 可能更适合;如果我们需要存储键值对并根据键进行查找,那么 HashMap 可能更适合。

HashSet 自动去重,因为它是用 HashMap 实现的,HashMap 的键是唯一的(哈希值),相同键的值会覆盖掉原来的值

HashSet 和 ArrayList 的区别
  • ArrayList 是基于动态数组实现的,HashSet 是基于 HashMap 实现的。
  • ArrayList 允许重复元素和 null 值,可以有多个相同的元素;HashSet 保证每个元素唯一,不允许重复元素,基于元素的 hashCode 和 equals 方法来确定元素的唯一性。
  • ArrayList 保持元素的插入顺序,可以通过索引访问元素;HashSet 不保证元素的顺序,元素的存储顺序依赖于哈希算法,并且可能随着元素的添加或删除而改变。
无序性和不可重复性的含义是什么
  • 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 不可重复性是指添加的元素按照 equals() 判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法。
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

Map

说一下 HashMap 的底层数据结构?

JDK 8 中 HashMap 的数据结构是数组+链表+红黑树

三分恶面渣逆袭:JDK 8 HashMap 数据结构示意图

HashMap 的核心是一个动态数组(Node[] table),用于存储键值对。这个数组的每个元素称为一个“桶”(Bucket),每个桶的索引是通过对键的哈希值进行哈希函数处理得到的。

当多个键经哈希处理后得到相同的索引时,会发生哈希冲突。HashMap 通过链表来解决哈希冲突——即将具有相同索引的键值对通过链表连接起来。

不过,链表过长时,查询效率会比较低,于是当链表的长度超过 8 时(且数组的长度大于 64),链表就会转换为红黑树。红黑树的查询效率是 O(logn),比链表的 O(n) 要快。数组的查询效率是 O(1)。

HashMap 的初始容量是 16,随着元素的不断添加,HashMap 的容量(也就是数组大小)可能不足,于是就需要进行扩容,阈值是capacity * loadFactor,capacity 为容量,loadFactor 为负载因子,默认为 0.75。

扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。

总的来说,HashMap 是一种通过哈希表实现的键值对集合,它通过将键哈希化成数组索引,并在冲突时使用链表或红黑树来存储元素,从而实现快速的查找、插入和删除操作。

HashMap扩容机制了解吗?
  1. 初始容量和负载因子: HashMap 在创建时会指定初始容量(默认为 16)和负载因子(默认为 0.75)。负载因子是一个控制哈希表空间利用率的参数,当 HashMap 中的元素个数超过 初始容量 * 负载因子 时,就会触发扩容。
  2. 扩容过程:
    • 当 HashMap 中的元素个数达到阈值(初始容量 * 负载因子),就会开始扩容。
    • 扩容过程是将哈希表的容量扩大为原来的两倍(即原来的容量乘以 2),同时重新计算每个元素的存储位置(通过重新哈希计算)。
    • 扩容过程中,HashMap 将所有的键值对重新分布到新的更大的哈希表中。这个过程是比较消耗资源的,因为需要重新计算哈希值和重新分配存储空间。
  3. 重新哈希计算:
    • 当 HashMap 扩容时,每个键值对的哈希值需要重新计算,因为扩容后哈希表的容量和哈希函数可能发生变化。
    • Java 使用 (hash & (newCapacity - 1)) 的位运算来确定元素在新表中的位置,这个公式通过利用新容量为2的幂的特性,快速计算元素的新位置。
  4. 扩容的性能影响:
    • 扩容是 HashMap 中一个性能开销较大的操作,因为它涉及到重新计算哈希值、重新分配存储空间和复制数据。
    • 在实际应用中,合理设置初始容量和负载因子可以减少扩容操作的频率,从而提升 HashMap 的性能。
解决哈希冲突有哪些方法呢?

①、再哈希法

准备两套哈希算法,当发生哈希冲突的时候,使用另外一种哈希算法,直到找到空槽为止。对哈希算法的设计要求比较高。

②、开放地址法

遇到哈希冲突的时候,就去寻找下一个空的槽。有 3 种方法:

  • 线性探测:从冲突的位置开始,依次往后找,直到找到空槽。
  • 二次探测:从冲突的位置 x 开始,第一次增加 12 个位置,第二次增加 22,直到找到空槽。
  • 双重哈希:和再哈希法类似,准备多个哈希函数,发生冲突的时候,使用另外一个哈希函数。

③、拉链法

也就是所谓的链地址法,当发生哈希冲突的时候,使用链表将冲突的元素串起来。HashMap 采用的正是拉链法。

HashMap与TreeMap区别

①、HashMap 是基于数组+链表+红黑树实现的,put 元素的时候会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,然后将元素插入到数组中,如果发生哈希冲突,会使用链表来解决,如果链表长度大于 8,会转换为红黑树。

get 元素的时候同样会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,如果遇到链表或者红黑树,会通过 key 的 equals 方法来判断是否是要找的元素。

②、TreeMap 是基于红黑树实现的,put 元素的时候会先判断根节点是否为空,如果为空,直接插入到根节点,如果不为空,会通过 key 的比较器来判断元素应该插入到左子树还是右子树。

get 元素的时候会通过 key 的比较器来判断元素的位置,然后递归查找。

由于 HashMap 是基于哈希表实现的,所以在没有发生哈希冲突的情况下,HashMap 的查找效率是 O(1)。适用于查找操作比较频繁的场景。

而 TreeMap 是基于红黑树实现的,所以 TreeMap 的查找效率是 O(logn)。并且保证了元素的顺序,因此适用于需要大量范围查找或者有序遍历的场景。

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() 值。

JDK 8 对 HashMap 主要做了哪些优化呢?为什么?

相比较 JDK 7,JDK 8 的 HashMap 主要做了四点优化:

①、底层数据结构由数组 + 链表改成了数组 + 链表或红黑树的结构。

原因:如果多个键映射到了同一个哈希值,链表会变得很长,在最坏的情况下,当所有的键都映射到同一个桶中时,性能会退化到 O(n),而红黑树的时间复杂度是 O(logn)。

②、链表的插入方式由头插法改为了尾插法。

原因:头插法虽然简单快捷,但扩容后容易改变原来链表的顺序。

③、扩容的时机由插入时判断改为插入后判断。

原因:可以避免在每次插入时都进行不必要的扩容检查,因为有可能插入后仍然不需要扩容。

HashMap 是线程安全的吗?多线程下会有什么问题?

HashMap 不是线程安全的,主要有以下几个问题:

①、多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素,在多线程的环境下,扩容的时候就有可能导致出现环形链表,造成死循环。

不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序。

②、多线程的 put 可能会导致元素的丢失。因为计算出来的位置可能会被其他线程的 put 覆盖。本来哈希冲突是应该用链表的,但多线程时由于没有加锁,相同位置的元素可能就被干掉了。

③、put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。

有什么办法能解决 HashMap 线程不安全的问题呢?

在 Java 中,有 3 种线程安全的 Map 实现,最常用的是ConcurrentHashMapCollections.synchronizedMap(Map)包装器。

Hashtable 也是线程安全的,但它已经不再推荐使用,因为 ConcurrentHashMap 提供了更高的并发性和性能。

①、HashTable 是直接在方法上加synchronized 关键字,比较粗暴。

②、Collections.synchronizedMap 返回的是Collections工具类的内部类。

③、ConcurrentHashMap在 JDK 7 中使用分段锁,在 JKD 8 中使用了CAS(Compare-And-Swap)+ synchronized 关键字,性能得到进一步提升。

LinkedHashMap 怎么实现有序的?

LinkedHashMap 维护了一个双向链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。

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,竞争会越来越激烈效率越低。

标签:Java,HashMap,框架,元素,数组,链表,线程,哈希,集合
From: https://blog.csdn.net/TIANLEeeee/article/details/141231261

相关文章

  • java异常你了解多少
    一、知识点概述(1)异常:异常就是Java程序在运行过程中出现的错误。(2)异常由来:问题也是现实生活中一个具体事务,也可以通过java的类的形式进行描述,并封装成对象。其实就是Java对不正常情况进行描述后的对象体现。(3)JVM的默认处理方案把异常的名称,错误原因及异常出现的位置等......
  • Java Data解决报错过程记录
    [attendancewebservice][24-08-1519:01:03.199][b3960aea15204b76b7c838189c28d45d][10.129.1.238]DEBUG[Thread-10][ne.jdbc.spi.SqlExceptionHelper.logExceptions139]couldnotexecutequery[select*fromid_customerswhereuserid=?]java.sql.SQLExceptio......
  • java7
    一、内部类1.成员内部类在一个类的内部定义的普通类可以访问外部类的所有成员,包括私有成员需要一个外部类的实例来创建成员内部类的实例可以被修饰为public、private、protected或者默认2.静态内部类一个静态内部类是静态的成员类。不需要外部类的实例来创建静态内部类......
  • 基于SSM框架的短视频网站的设计与实现
    随着科学技术的发展,人们对服务的要求也越来越高。为了能提高管理者的管理效能,现在的短视频网站管理必须要脱离复杂的手工管理方式。随着信息化时代的到来,智能操作系统成为短视频网站的重要组成部分,为用户提供优质的服务。该系统采用Java编程语言,采用开放源码系统结构SSM完成......
  • 实现同时接收文件与实体类,java springboot maven
    首先,需要有一个Post接口,有一个实体类方法需要返回什么,直接修改void即可实体类需要接收什么,直接改User即可 @PostMapping(value="/post_interface")publicvoidpostInterface(@RequestParam("file")MultipartFilefile,@RequestParamMap<String,Object>user){......
  • Java的三大使用平台
    一、Java的三大使用平台:1.JavaSE2.JavaME3.JavaEE 二、JavaSEJavaSE用于桌面应用的开发,是其它2个版本的基础。何谓桌面应用?用户只要打开程序,程序的界面会让用户在最短的时间内找到他们需要的功能,同时主动带领用户完成他们的工作并得到最好的体验。学习JavaSE的目的......
  • 【日常记录-Java】EasyExcel输出设定字体
    Author:赵志乾Date:2024-08-15Declaration:AllRightReserved!!!1.问题描述    使用EasyExcel默认的设定输出时,中文字体显得比较怪异。2.解决方案    本质是单元格样式的设置问题,在EasyExcel中,可以通过实现WriteHandler接口或使用EasyExcel提供的注解以及W......
  • java并发
    线程的生命周期(线程有几种状态)线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态:新建状态(New):新创建了一个线程对象。就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。运行状态(Ru......
  • 【Java】 IO流使用方法 (常见方法)
    Java系列文章目录补充内容Windows通过SSH连接Linux第一章Linux基本命令的学习与Linux历史文章目录Java系列文章目录一、前言二、学习内容:三、问题描述四、解决方案:4.1File的使用4.2防止乱码问题五、总结:5.1学习总结:一、前言学习文件IO流学习文档的使用具......
  • 使用dotenv保护JavaScript代码中的秘密信息
    把诸如apikey这种秘密信息写死的源代码里不可取,比如通常源代码会通过git仓库等进行管理,这样敏感信息就会被共享了。我们选择使用dotenv库把敏感信息配置在.env文件中,然后把.env文件添加到gitignore文件里,不上传到代码仓库。node程序启动后,会将.env文件里的配置项加载到进程对应......