当前位置: 首页 > news >正文

【Java集合】List,Map,Set-详细讲解

文章目录

  • Java 集合框架核心知识点总结
    • 概念
      • 数组和集合的区别
      • Collection 和 Collections 有什么区别
    • List
      • List 中的几种实现,以及他们的不同
      • List 可以一边遍历一边修改元素吗
      • ArrayList 线程不安全体现在哪里?
      • ArrayList 的扩容机制
    • Map
      • Map 的遍历方式
      • HashMap 的实现原理
      • 解决哈希冲突的方法有哪些
      • HashMap 是线程安全的吗,可能会出现哪些问题?
      • HashMap 的 put 方法的过程
      • HashMap 一般使用什么作为 key?为什么
      • 为什么 HashMap 使用红黑树而不是直接使用平衡二叉树
      • HashMap 的扩容机制
      • 说一说 HashMap 的负载因子
      • ConcurrentHashMap 是怎么实现的
      • HashTable 的实现原理
      • HashMap, HashTable, ConcurrentHashMap 的区别
    • Set
      • Set 集合有什么特点,怎么实现的
      • 有序的 set 集合是什么?

Java 集合框架核心知识点总结

概念

数组和集合的区别

  • 数组的长度是不可变的,一旦被创建,不能被修改。而集合的长度是可变的
  • 数组中存储的元素可以是基本数据类型,也可以是对象。而集合中存储的元素只能是对象
  • 数组可以通过下标直接访问到元素,而集合需要通过迭代器等方式来获取元素。

Collection 和 Collections 有什么区别

  • Collection 是集合类的总接口,其中定义了一些通用的方法比如增加删除元素,ListSet 都是它的实现类。
  • Collections 是 Java 提供的用来处理集合的工具包,位于 java.util 包中。

List

List 中的几种实现,以及他们的不同

  • ArrayList:它是由可变数组实现的,是线程不安全的。它的元素访问速度比较快,但是对元素的增加和删除速度比较慢
  • LinkedList:基于双向链表实现的,也是线程不安全的。它的元素访问速度比较慢但是元素的增加和删除速度比较快
  • Vector:类似与 ArrayList 也是可变数组,它是线程安全的。
  • CopyOnWriteArrayList:它也是线程安全的,它通过对写操作进行加锁,保证了线程安全。对读操作没有加锁,这使得读写分离,可以在修改的同时不影响读操作。
实现类底层结构线程安全访问速度增删速度
ArrayList可变数组
LinkedList双向链表
Vector可变数组
CopyOnWriteArrayList可变数组

List 可以一边遍历一边修改元素吗

这与遍历的方式有关:

  • 使用普通 for 循环遍历:可以直接修改。

    for (int i = 0; i < list.size(); i++) {list.set(i, "newValue"); // 可以直接修改
    }
    
  • 使用增强型 for 循环 (foreach)不可以直接修改,因为它的底层是基于迭代器实现的,强行修改可能会导致 ConcurrentModificationException

    for (String item : list) {list.remove(item); // 可能抛出 ConcurrentModificationException
    }
    
  • 使用迭代器遍历:可以使用迭代器的 remove(), set() 方法来进行删除和修改操作。

    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {String item = iterator.next();iterator.remove(); // 安全删除// iterator.set("newValue"); // 安全修改
    }
    

ArrayList 线程不安全体现在哪里?

  • 可能会出现 null 值ArrayList 底层实现 add 方法是先判断当前位置是否超过容量大小,如果没有将值放入进去,然后将当前位置对应的变量 size++。如果此时两个线程,线程1先判断当前 size 为9,未超过容量,则执行 set 操作,还没进行 size++ 时就转换到线程二进行操作,同样判断当前位置为9,进行 set 操作。这时候两个线程再分别进行 size++ 操作,这时候 size=11,导致 size=10 这个位置为 null
  • 可能越界:同样在执行 add 操作时,线程一执行完 set 操作后还没有进行 size++,线程二就去判断是否超过容量,结果发现没有超过容量,但是还没有执行 set 操作就进入线程1进行 size++,这时候线程二执行 set 操作就会发生越界。
  • size 与 add 的数量不一致:这是由于 size++ 这个操作本身就不是原子性的,它分为获取 size 值,将其进行 +1,将新值赋值给 size。可能线程1和线程2拿到一样的 size 然后互相覆盖掉了。

ArrayList 的扩容机制

当内部数组容量不够时,会触发扩容机制。

  1. 计算扩容后数组的大小:新容量一般为旧容量的 1.5 倍
  2. 创建新数组:根据计算得到的大小创建新数组。
  3. 数据复制:将原数组当中的数据复制到新数组当中。
  4. 更新引用:将指向原数组的引用改为指向新数组。

Map

Map 的遍历方式

  • 通过 for-eachentrySet 来进行遍历
    for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    
  • 通过 for-eachkeySet 来进行遍历
    for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key));
    }
    
  • 通过 entrySet 和迭代器来进行遍历
    Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()) {Map.Entry<String, Integer> entry = iterator.next();System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    
  • 通过 forEach 方法和 Lambda 表达式进行遍历
    map.forEach((k, v) -> System.out.println(k + ": " + v));
    
  • 通过 Stream API 进行遍历
    map.entrySet().stream().forEach(entry -> {System.out.println(entry.getKey() + ": " + entry.getValue());
    });
    

HashMap 的实现原理

HashMapjdk1.7 以前是由数组+链表实现的,通过计算 keyhashcode 来将其映射到数组当中的插槽位置。如果多个 key 计算得到的插槽位置相同,那么使用链表将其连接起来存放在插槽中。
jdk1.8 以后是由数组+链表+红黑树实现的。当链表长度大于 8,数组长度大于 64 时,会将链表转化为红黑树,提高查询速率。

解决哈希冲突的方法有哪些

  • 再哈希法:当发生哈希冲突时,我们可以使用其他 hash 函数来再次计算。
  • 连接法:通过使用链表等数据结构将其连接在一起,放在同一个哈希桶中(HashMap 采用的方法)。
  • 开放定址法:通过使用线性探测等算法重新找到位置存放。
  • 哈希桶扩容

HashMap 是线程安全的吗,可能会出现哪些问题?

不是,在 jdk1.7 以前,多线程环境下可能在增加元素时产生循环链表,导致 entrykey 死循环,还可能发生数据丢失的问题。在 jdk1.8 以后,使用数组+链表+红黑树的结构,解决了 entrykey 链死循环的问题。但是还是有 put 方法的覆盖问题

HashMap 的 put 方法的过程

  1. 根据要存放元素的 key 计算 hashcode 找到插槽位置。
  2. 判断该位置是否存在元素,如果不存在,直接创建一个 entry 对象来存储该键值对,并将修改次数 +1
  3. 如果存在元素,进一步通过 equals 方法判断是否相同。如果相同直接将其 value 进行更新。如果不相同则遍历红黑树或者链表,查看是否有相同的元素,从而进行更新或者插入操作。
  4. 检查链表长度是否大于阈值(是否为 8)。
  5. 检查负载因子是否大于 0.75,如果大于进行扩容。

HashMap 一般使用什么作为 key?为什么

String,因为 String不可变的,一旦被创建不能被修改,它的 hashcodeequals 方法都是固定的,较为稳定。

为什么 HashMap 使用红黑树而不是直接使用平衡二叉树

  • 平衡二叉树它要求每个子树的高度差不能超过 1,这个要求太严格了。会频繁修改树的结构
  • 红黑树要求最长结点长度不能超过最短节点的 2 倍,虽然牺牲了一部分查询效率,但是不会频繁触发修改树的结构
特性红黑树平衡二叉树 (AVL)
平衡标准非严格平衡(最长路径不超过最短路径的2倍)严格平衡(左右子树高度差绝对值不超过1)
查询效率O(log n),稍弱于 AVLO(log n),查询性能极致
插入/删除效率更高,旋转次数少,调整操作更少效率较低,需要更频繁的旋转操作以维持绝对平衡
适用场景增删操作频繁的场景(如 HashMap)查询非常多,增删很少的场景(如数据库索引)

HashMap 的扩容机制

当负载因子大于 0.75,即元素数量与数组大小的比值为 0.75 时会触发扩容。扩容一般分为两步:

  1. 计算扩容后的长度并创建数组:新容量为旧容量的 2 倍
  2. 将旧数组中的数据转移到新数组
    • jdk1.7 以前,转移操作是直接将数组重新进行一个一个的哈希运算,重新分配位置。
    • jdk1.8 以后,通过将该元素当前位置 & 扩容后数组大小得到 0 或者 1,如果为 0 则还在原位置,如果为 1 则在该位置 + 原数组大小的位置。这样可以不用通过哈希运算快速找到新位置。极大提高效率。

说一说 HashMap 的负载因子

负载因子是指元素的数量与数组大小的比值。当大于 0.75 时会触发扩容。设置为 0.75 的原因是平衡了时间和空间复杂度。负载因子过大,会导致大量碰撞,导致性能降低。过小会导致内存利用率不高。

ConcurrentHashMap 是怎么实现的

  • jdk1.7 以前,是由数组+链表实现的。并且通过 Segment(可重入锁)将数据分为一段一段的,并且为每一段数据分配一把锁。当一个线程访问一段数据时,其他段的数据也能被其他线程所访问。
  • jdk1.8 以后采用数组+链表+红黑树实现。并且通过 volatile + synchronizedCAS 实现的。当向其中添加一个元素时,会先判断容器是否被创建,如果没有,就使用 volatileCAS(乐观锁)来初始化容器。然后判断容器内该位置是否有元素,如果没有,直接使用 CAS 创建元素(乐观锁),如果有,使用 synchronized 关键字,然后遍历链表或者红黑树,进行修改或者创建元素。

HashTable 的实现原理

  • HashTable 底层是通过数组+链表实现的,数组是本体,链表是为了解决哈希冲突的。
  • HashTable 通过将内部方法都用 synchronized 关键字进行修饰,来实现线程安全的。

HashMap, HashTable, ConcurrentHashMap 的区别

特性HashMapHashTableConcurrentHashMap (JDK 1.8)
线程安全 ( synchronized 方法) ( synchronized + CAS)
Key/Value允许null不允许null不允许null
性能低 (全局锁)高 (锁粒度小)
底层结构数组+链表+红黑树 (JDK8+)数组+链表数组+链表+红黑树 (JDK8+)
扩容2倍2n+12倍
初始容量161116
  • HashMap 是非线程安全的,它的 keyvalue 允许为 null。默认初始容量为 16,扩容时一般扩容为原来的 2 倍。如果设置了初始值,扩容为 2 的幂次倍。它的底层是由链表+数组+红黑树实现的。
  • HashTable 是线程安全的,它的 keyvalue 不允许为 null。默认初始容量为 11,扩容时一般扩容为原来的 2n+1。如果设置了初始值,直接使用初始值。它的底层是数组+链表实现的。
  • ConcurrentHashMap 是线程安全的,它的 keyvalue 不允许为 null。它可以在多线程环境下进行并发读写。它的底层是由数组+链表+红黑树实现的。

Set

Set 集合有什么特点,怎么实现的

特点:无重复。
通过内部的数据结构来实现 key 的无重复,首先它会根据 hash 值来判断该元素在 set 中是否存在,如果存在,进一步使用 equals 方法进行判断。

有序的 set 集合是什么?

  • TreeSet:通过比较器中的比较规则来进行有序排序
  • LinkedHashSet:通过链表来记录插入的顺序
http://www.dtcms.com/a/342151.html

相关文章:

  • Android基建开发大纲
  • Android - 资源类型 MINE Type
  • Android15 AndroidV冻结和解冻的场景
  • Android URC 介绍及源码案例参考
  • vlc-android: 编译自己的libvlc
  • MySQL深分页慢问题及性能优化
  • thingsboard 通过Entities hierarchy部件实现左边菜单点击,右边的表格按左边的分类型进行过滤筛选数据源
  • 什么是Qoder?如何下载?如何体验?Qoder和其他 AI IDE 什么区别?
  • ZStack Zaku替代VMware Tanzu:六项对比、构建虚拟机+容器一体化架构
  • C# 编写一个XmlToDota的转换工具
  • 关于CentOS7无法使用使用
  • 在Java项目中去理解通用工具为什么能通用,以及如何写出类似的工具类
  • 实践题:智能化风控体系升级方案
  • 医疗器械注册证识别技术:实现从图像到结构化数据的智能转化,提升行业效率与准确性
  • 深度解析游戏引擎中的相机:视图矩阵
  • 【数据结构】深入解析选择排序与堆排序:从基础到高效实现的完全指南
  • 如何在Docker配置中启用实验性模式
  • 对实验室管理而言,LIMS系统究竟有无作用
  • 【STM32】HAL库中的实现(八):I2C通信(以 AT24C02 为例)
  • CentOS系统安装Git全攻略
  • 面试准备革命:面试汪 vs 传统方法,谁更胜一筹?
  • 「数据获取」《中国环境统计年鉴》(1998-2024)(获取方式看绑定的资源)
  • Linux命令大全-userdel命令
  • awk 命令的使用
  • 《P2700 逐个击破》
  • Design Compiler:逻辑库名与逻辑库文件名及其指定方式
  • 自学嵌入式第二十四天:数据结构(4)-栈
  • JetBrains Mono字体
  • Django ModelForm
  • 用 Python 写的自动化测试 WPF 程序的一个案例