Java 集合基础
Java 集合基础
常用的集合分类以及它们的区别?
回答:
Java 中的集合主要分为两大体系:Collection 和 Map。其中 Collection 包含 List、Set 和 Queue 三类,用于存储元素集合;Map 则用于存储键值对数据,每个键唯一对应一个值。
分析:
在 Java 中,集合框架是对数据结构的高度抽象与实现统一封装的体系。
Collection 接口体系下的 List 表示有序可重复的线性表结构,如 ArrayList、LinkedList;
Set 表示无序且不可重复的集合,常见的有 HashSet、TreeSet;Queue 通常用于实现队列、堆等结构,如 PriorityQueue、LinkedList。
在另一方面,Map 结构则用于维护 key-value 键值对,它的核心特性是 key 的唯一性和映射性,代表实现有 HashMap、TreeMap、LinkedHashMap 等。Map 不属于 Collection 接口派生,而是自成体系。
集合的选型往往依赖于应用场景的要求,例如是否需要顺序保持、是否允许重复、是否对并发友好等。掌握各类集合的基本特性,是理解 Java 集合框架的第一步,也为后续高阶使用(如并发容器、内存优化等)打下基础。
哪些集合类是线程安全的?
线程安全集合是并发编程中处理共享数据的关键。
早期的 Vector、Stack 和 Hashtable 是通过方法级别的 synchronized 保证线程安全,但由于其锁粒度较大、性能不佳,逐渐被更高效的并发集合取代。
JDK1.5 引入了 java.util.concurrent 包,提供了一系列基于分段锁(如 ConcurrentHashMap)和写时复制机制(如 CopyOnWriteArrayList、CopyOnWriteArraySet)的线程安全集合,它们在保证并发性的同时最大程度减少了锁竞争,提高了程序的吞吐能力。
此外,还有如 BlockingQueue、ConcurrentSkipListMap 等结构,在特定场景(如任务队列、并发排序)中也被广泛应用。在高并发系统中,应优先使用这些现代并发集合,而非传统的同步集合类。
什么是 fail-fast,什么是 fail-safe?
fail-fast
是迭代器检测到集合被修改时立即抛出异常的机制,目的是及时暴露问题,避免数据不一致导致的不可预期行为。
核心特性 如下:
检测机制:迭代器内部维护修改计数器
modCount
,记录集合结构性修改(如添加、删除元素)的次数。迭代时,每次操作都会检查当前集合的modCount
与迭代器初始化时记录的expectedModCount
是否一致:一致则继续迭代;不一致则立即抛出ConcurrentModificationException
,终止迭代。适用场景:非线程安全的集合如
ArrayList
、HashMap
、HashSet
的迭代器默认采用这一机制。局限性:仅能检测并发修改,无法保证一定捕获(单线程中迭代时通过集合自身方法修改,而非迭代器的
remove
,也会触发异常);不适合多线程场景,因为多线程修改可能导致modCount
校验失效;迭代过程中不允许通过集合自身方法修改结构,但允许通过迭代器的remove
修改(会同步更新expectedModCount
)。
fail-safe
是迭代器基于集合“快照”进行操作的机制,即使集合被修改,也不会影响当前迭代,从而避免抛出异常。
核心特性如下
实现逻辑:迭代器创建时会复制一份集合的底层数据(如数组或链表),迭代操作基于这份快照进行。因此,即使原集合在迭代过程中被修改,迭代器也不会感知,仍能正常完成遍历。
适用场景:线程安全的集合如
ConcurrentHashMap
、CopyOnWriteArrayList
的迭代器采用这一机制。局限性:内存开销大:需要复制集合数据,大容量集合可能占用双倍内存;迭代结果可能不是最新的:基于快照,无法反映迭代过程中集合的实时修改;性能较低:复制数据和维护快照会增加额外的时间和空间成本。
关键区别对比 :
维度 | fail-fast | fail-safe |
---|---|---|
核心机制 | 检测到修改立即抛异常 | 基于快照迭代,不感知修改 |
异常抛出 | 会抛出ConcurrentModificationException | 不会抛出异常 |
内存开销 | 低(无需复制数据) | 高(需复制集合快照) |
迭代数据时效性 | 反映实时数据(但可能因修改中断) | 反映快照数据(可能不是最新) |
典型适用集合 | ArrayList 、HashMap 、HashSet | ConcurrentHashMap 、CopyOnWriteArrayList |
线程安全支持 | 不支持(多线程修改可能漏检) | 支持(基于快照避免并发冲突) |
fail-fast
的设计目标是“及时暴露错误”,适用于单线程场景,通过牺牲迭代连续性换取问题的快速发现;fail-safe
的设计目标是“保证迭代安全”,适用于多线程场景,通过牺牲内存和时效性换取迭代的稳定性。实际开发中,需根据是否涉及多线程、对内存和时效性的要求选择合适的集合及迭代机制。
fail-fast 快速失败机制底层是怎么实现的?
fail-safe 机制的核心是基于集合快照迭代,通过隔离迭代数据与原集合的修改,避免并发冲突,具体流程如下:
创建迭代器时复制快照
采用 fail-safe 机制的集合(如 ConcurrentHashMap、CopyOnWriteArrayList)在创建迭代器时,会对集合的底层数据(如数组、链表)进行一次复制,生成一份份“快照”。迭代器所有操作(如next()
、hasNext()
)都基于这份快照进行,而非原集合。迭代与原集合修改完全隔离
迭代过程中,若其他线程或当前线程修改原集合(添加、删除元素等),只会影响原集合的底层数据,而迭代器依赖的快照不会不会发生变化。因此,迭代器无法感知原集合的修改,也不会抛出异常,能正常完成遍历。线程安全的保证
快照的复制过程通过线程安全机制实现(如 CopyOnWriteArrayList 的ReentrantLock
锁、ConcurrentHashMap 的 CAS 操作),确保复制期间原集合数据的一致性。同时,由于迭代基于快照,避免了多线程下的资源竞争。
Collection 和 Collections 有什么区别?
Collection 是 Java 集合框架中最核心的接口之一,它是 List、Set、Queue 等接口的父接口,定义了如 add()
、remove()
、iterator()
等操作方法,用于规范各种集合类型的基本行为。
它本身不能被实例化,但为 List、Set 等集合实现类提供了统一的操作规范。而 Collections 是一个 final 类,不能被继承,它提供了一系列静态方法,用于对集合进行辅助操作,比如 Collections.sort(list)
排序、Collections.synchronizedList(list)
返回线程安全包装等。
一个是接口,用于描述集合的行为规范;一个是工具类,用于操作集合对象。两者在名称上相似,但角色截然不同,不能混淆使用。掌握它们之间的区别,是熟练使用集合框架的基础。
集合遍历的方法有哪些?
Java 集合的遍历方法主要有六种,各有适用场景和特点:
- foreach
基于迭代器实现,语法简洁:
List<String> list = Arrays.asList("a", "b", "c");
for (String str : list) {
System.out.println(str);
}
适用于所有实现 Iterable 接口的集合,可读性高,但遍历中不能修改集合结构,否则会抛异常,且不支持获取索引。
- 迭代器(Iterator)
通过iterator()获取迭代器,用hasNext()和next()遍历:
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
if (num == 2) {
iterator.remove(); // 安全删除
}
}
支持在遍历中通过iterator.remove()安全删除元素,适用于所有 Collection 接口集合,是 foreach 的底层实现。
- for 循环(索引遍历)
List<Double> list = new ArrayList<>(Arrays.asList(1.1, 2.2, 3.3));
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
仅适用于 List 集合,支持获取索引,对 ArrayList 效率高(随机访问快),但对 LinkedList 效率低(需遍历索引)。
- ListIterator(双向迭代器)
List 特有的迭代器,支持正向和逆向遍历:
List<String> list = new ArrayList<>(Arrays.asList("x", "y", "z"));
ListIterator<String> listIt = list.listIterator();
// 正向遍历
while (listIt.hasNext()) {
System.out.println(listIt.next());
}
// 逆向遍历
while (listIt.hasPrevious()) {
System.out.println(listIt.previous());
}
可通过add() set() remove()修改元素,还能获取元素索引,功能丰富但仅适用于 List 集合。
- Lambda 表达式(forEach 方法,Java 8+)
通过集合.forEach(...)遍历:
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.keySet().forEach(key -> System.out.println(key + ":" + map.get(key)));
代码简洁,适合简单逻辑,遍历中不能修改集合结构,适用于所有 Iterable 接口集合。
- 并行流(parallelStream,Java 8+)
通过多线程并行遍历:
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
list.parallelStream().forEach(num -> {
// 注意线程安全
System.out.println(Thread.currentThread().getName() + ":" + num);
});
遍历方式 | 适用集合 | 支持修改集合 | 索引访问 | 线程安全 | 特点总结 |
---|---|---|---|---|---|
foreach | 所有 Iterable | 否 | 否 | 否 | 简洁常用,禁止结构修改 |
Iterator | 所有 Collection | 是(迭代器删) | 否 | 否 | 支持安全删除,底层实现 |
普通 for 循环 | 仅 List | 是 | 是 | 否 | 依赖索引,ArrayList 高效 |
ListIterator | 仅 List | 是(增删改) | 是 | 否 | 双向遍历,功能最丰富 |
Lambda 表达式 | 所有 Iterable | 否 | 否 | 否 | 函数式风格,简洁 |
并行流 | 所有 Collection | 需谨慎 | 否 | 需手动保证 | 多线程并行,适合大数据量 |
怎么确保一个集合不能被修改?
在某些业务场景中,我们希望暴露出去的集合只允许读取而不能被修改,以避免数据被误操作破坏。这时可以通过 JDK 提供的 Collections.unmodifiableXXX
方法对集合进行包装,例如 unmodifiableList
、unmodifiableSet
、unmodifiableMap
等。这些方法并不创建新集合,而是返回原集合的一个只读视图,只要尝试调用如 add()
、remove()
等修改操作,JVM 就会抛出运行时异常,确保集合结构不被篡改。例如:
List<String> list = new ArrayList<>();
list.add("x");
Collection<String> readOnlyList = Collections.unmodifiableCollection(list);
readOnlyList.add("y"); // 运行时抛出 UnsupportedOperationException
需要注意的是,这种只读保护只针对集合本身结构的修改,若集合中存储的是可变对象,则对象内部的状态依然可能被修改,称为"浅不可变"。如果希望实现完全不可变,需结合不可变对象使用或自行深拷贝集合内容。
