Java全栈面试题汇总目录

目录

集合概念

什么是集合?

使用集合框架的好处?

Collection和Collections有什么区别?

Iterator是什么?有什么作用?

Iterable和Iterator区别?

常用的集合类有哪些?

List,Set,Map的区别,存取元素时,各有什么特点?

集合框架底层数据结构?

哪些集合类是线程安全的?

介绍下fail-fast与fail-safe区别?

怎么确保一个集合不能被修改?

如何边遍历边移除Collection中的元素?

Comparable和Comparator区别?

Collections工具类中的sort()方法如何比较元素?

List

List接口有什么特点?

遍历一个List有哪些不同的方式,每种方法的实现原理是什么,Java中List遍历的最佳实践是什么?

ArrayList的优缺点?

如何实现数组和List之间的转换?

ArrayList和LinkedList的区别是什么?

ArrayList和Vector的区别是什么?

ArrayList扩容机制?

插入数据时,ArrayList、LinkedList、Vector谁速度较快,阐述ArrayList、Vector、LinkedList的存储性能和特性?

多线程场景下如何使用ArrayList?

为什么ArrayList的elementData加上transient修饰?

Set

Set接口有什么特点?

HashSet的实现原理?

HashSet、LinkedHashSet、TreeSet区别?

TreeSet如何排序?

Map

Map接口有什么特点?

HashMap的实现原理?

HashMap在JDK1.7和JDK1.8中有哪些不同?

能否使用任何类作为Map的key?

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

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

HashMap、HashTable、TreeMap、LinkedHashMap特点与区别?

HashMap线程不安全表现?

TreeMap如何排序?

HashSet与HashMap的区别?

Queue

Queue接口有什么特点?

Queue和Deque有什么区别?

Queue中add()、offer()、remove()、poll()、element()、peek()的区别?

常见Queue实现类有哪些?

PriorityQueue实现原理?

为什么不推荐使用Stack?推荐用什么?

ArrayDeque与LinkedList区别?

Queue和List的区别?

并发集合

为什么要使用并发集合?

ConcurrentHashMap的实现机制?

ConcurrentHashMap与HashMap和Hashtable的差异?

ConcurrentHashMap是弱一致性的体现?

什么是CopyOnWriteArrayList?适用场景?

什么是阻塞队列BlockingQueue?

ConcurrentLinkedQueue原理?和BlockingQueue的对比?

ConcurrentSkipListMap/ConcurrentSkipListSet特点?

多线程遍历集合为什么会抛ConcurrentModificationException?


集合概念

什么是集合?

集合是Java用来存放、管理对象数据的容器,位于java.util包下。

数组和集合的区别

  • 数组可以存数据,但长度固定、操作麻烦;
  • 集合就是长度可变、功能更强大的“动态数组/容器”。

核心特点

  1. 只能存储引用类型(对象),不能存基本数据类型(要存要用包装类)
  2. 容量动态变化,用多少自动扩多少
  3. 提供丰富的API:增、删、改、查、遍历、判断等
  4. 有多种结构,适应不同场景(List、Set、Map等)

集合与数组的区别

  • 数组:固定长度,可存基本类型,功能单一
  • 集合:可变长度,只能存对象,方法丰富

使用集合框架的好处?

1.容量动态扩容

不用预先固定长度,随数据自动扩容,比数组灵活。

2.丰富的数据结构实现

提供List、Set、Map、Queue等多种实现,直接使用,无需自己手写。

3.完善的API,简化开发

内置增删改查、排序、遍历、批量操作等方法,代码更简洁。

4.高性能优化

底层经过精心设计(如哈希表、动态数组、红黑树),查询、插入效率高。

5.工具类支持

配合Collections、Arrays实现排序、同步、反转等通用操作。

6.泛型支持,类型安全

编译期检查类型,避免类型转换异常。

7.线程安全实现

提供ConcurrentHashMap、CopyOnWriteArrayList等,适配多线程场景。

8.统一标准,易于维护

一套通用接口和设计规范,代码可读性、可移植性更强。

Collection和Collections有什么区别?

1.Collection

  • 是父接口(java.util.Collection)
  • 是List、Set、Queue的顶层父接口
  • 定义了单列集合的通用方法:add、remove、clear、contains、size等
  • 本身是接口,不能直接实例化

2.Collections

  • 是一个工具类(java.util.Collections)
  • 专门用来操作集合的静态工具方法
  • 提供很多静态方法:sort、shuffle、reverse、binarySearch、synchronizedCollection等
  • 构造器私有,不能实例化,直接调用静态方法

Iterator是什么?有什么作用?

是什么

  • Iterator叫迭代器,是Java集合框架中的一个接口。
  • 位于java.util.Iterator
  • 用来**遍历集合(Collection系列:List、Set等)**中的元素。

核心作用

1.统一遍历方式

不管是ArrayList、LinkedList还是HashSet,都用同一种迭代方式:hasNext()→next()

2.安全遍历并删除元素

在遍历过程中,通过迭代器的remove()方法删除元素,不会抛并发修改异常。

3.隐藏集合内部结构

使用者不用关心集合是数组还是链表,直接迭代即可。

常用方法

  • hasNext():判断是否还有下一个元素
  • next():获取下一个元素
  • remove():删除刚遍历过的元素

简单示例

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

扩展

  • 增强for循环底层就是Iterator
  • 不能用集合自身的remove()遍历删除,否则会触发ConcurrentModificationException(fail-fast机制)
  • ListIterator是List特有的迭代器,支持向前遍历、添加、修改元素

Iterable和Iterator区别?

定位

  • Iterable是“可迭代的”,表示这个集合具备被遍历的资格。
  • Iterator是“迭代器”,是真正执行遍历的工具。

区别

1.定义不同

  • Iterable:是一个接口,java.lang.Iterable

  • Iterator:是一个接口,java.util.Iterator

2.核心方法不同

  • Iterable:只有一个方法:

Iterator<T> iterator();

作用:返回一个迭代器对象。

  • Iterator 有三个核心方法:

boolean hasNext();
T next();
void remove();

作用:真正用来遍历、取下一个元素、删除元素。

3.功能定位不同

  • Iterable:表示“这个集合可以被迭代”→所有Collection(List/Set/Queue)都实现了Iterable

  • Iterator:表示“用来迭代的工具”→每次迭代都会创建一个新的Iterator实例

4.和增强for循环的关系

  • 只有实现了Iterable的类,才能用:

for (T t : 集合) { ... }

底层就是通过iterator()获取迭代器来遍历。

常用的集合类有哪些?

单列集合Collection

1.List体系(有序、可重复)

  • ArrayList
  • LinkedList
  • Vector(老、线程安全,现在少用)
  • CopyOnWriteArrayList(并发)

2.Set体系(无序、不可重复)

  • HashSet
  • LinkedHashSet
  • TreeSet

双列集合Map(key-value)

  • HashMap
  • LinkedHashMap
  • TreeMap
  • HashTable(老、线程安全)
  • ConcurrentHashMap(高并发常用)

List,Set,Map的区别,存取元素时,各有什么特点?

体系

  • List、Set继承自Collection(单列集合,存单个元素)
  • Map是独立接口(双列集合,存key-value键值对)

核心区别

1.List(有序、可重复、有索引)

特点

  • 元素有序(存入顺序=取出顺序)
  • 元素可重复
  • 有下标索引,可以通过index操作

常见实现

  • ArrayList(数组,查询快)
  • LinkedList(链表,增删快)

存取特点

  • 存:直接添加,允许重复
  • 取:可按索引get(index)取,也可遍历

2.Set(无序、不可重复、无索引)

特点

  • 元素无序(存取顺序不一定一致)
  • 元素不可重复(自动去重)
  • 没有索引,不能通过下标访问

常见实现

  • HashSet(哈希表,无序)
  • TreeSet(红黑树,可排序)
  • LinkedHashSet(有序+去重)

存取特点

  • 存:自动根据hashCode+equals去重
  • 取:只能遍历,不能按位置取

3.Map(键值对、key唯一、value可重复)

特点

  • 存储key-value键值对
  • key不可重复,value可重复
  • key无序(取决于实现类)

常见实现

  • HashMap(哈希表,线程不安全)
  • TreeMap(可排序)
  • LinkedHashMap(保证插入顺序)
  • ConcurrentHashMap(线程安全)

存取特点

  • 存:put(key,value),key重复会覆盖旧value
  • 取:通过key快速get(key)查找value

总结

  • List:有序、可重复、有索引,查找方便
  • Set:无序、不可重复、无索引,用于去重
  • Map:存储键值对,key唯一,适合快速查找

集合框架底层数据结构?

List

  • ArrayList:动态数组
  • LinkedList:双向链表(JDK6是循环,JDK7+非循环)
  • Vector:动态数组(线程安全,效率低)

Set

  • HashSet:HashMap(把元素存在key上)
  • LinkedHashSet:LinkedHashMap(哈希表+双向链表)
  • TreeSet:TreeMap(红黑树)

Map

  • HashMap:JDK7:数组+链表,JDK8+:数组+链表/红黑树(链表长度≥8转红黑树,≤6退链表)
  • LinkedHashMap:HashMap+双向链表(保证有序)
  • TreeMap:红黑树(可排序)
  • Hashtable:数组+链表(线程安全,低效)
  • ConcurrentHashMap:JDK7:分段锁(Segment)+数组+链表,JDK8+:数组+链表/红黑树+CAS+synchronized

Queue

  • ArrayDeque:动态数组
  • PriorityQueue:堆(优先级队列)

哪些集合类是线程安全的?

1.传统线程安全集合

  • Vector
  • Stack(继承自Vector)
  • Hashtable

它们的方法都加了synchronized,但粒度粗、并发性能差,现在基本不推荐使用。

2.并发包下高性能线程安全集合

java.util.concurrent下的,高并发首选

List

  • CopyOnWriteArrayList

Set

  • CopyOnWriteArraySet
  • ConcurrentSkipListSet(有序)

Map

  • ConcurrentHashMap
  • ConcurrentSkipListMap(有序)

3.通过Collections工具类获得的安全集合

可以把普通集合变成线程安全的:

Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedSet(new HashSet<>());
Collections.synchronizedMap(new HashMap<>());

底层也是用synchronized,性能一般。

介绍下fail-fast与fail-safe区别?

1.概念区别

fail-fast(快速失败)

迭代器遍历集合时,一旦检测到集合被修改(增/删/改),立刻抛出ConcurrentModificationException,不继续执行。

fail-safe(安全失败)

迭代器遍历的是原集合的副本,遍历时修改原集合不会影响遍历,不抛异常,但可能读到旧数据。

2.底层原理

fail-fast

  • 遍历前记录集合的modCount(修改次数)
  • 每次遍历都对比modCount是否变化
  • 变化→直接抛异常

fail-safe

  • 不直接操作原集合,而是拷贝一份快照(clone)遍历
  • 原集合修改不影响快照,因此不会抛并发修改异常

3.代表集合

fail-fast

ArrayList、HashMap、HashSet等非线程安全集合

fail-safe

JUC包下的并发集合:

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • CopyOnWriteArraySet

4.优缺点对比

特性 fail-fast fail-safe
异常 抛ConcurrentModificationException 不抛异常
遍历对象 直接遍历原集合 遍历集合副本
数据一致性 强一致性,不允许脏数据 弱一致性,可能读到旧数据
内存开销 大(需要拷贝数组/结构)
适用场景 单线程、强一致性场景 高并发、允许短暂不一致的场景

怎么确保一个集合不能被修改?

1.使用Collections.unmodifiableXXX(最常用)

包装成不可修改视图,任何增删改都会抛UnsupportedOperationException。

List<String> list = new ArrayList<>();
List<String> unmodList = Collections.unmodifiableList(list);

Set<String> set = Collections.unmodifiableSet(new HashSet<>());
Map<String, Object> map = Collections.unmodifiableMap(new HashMap<>());

2.使用JDK9+集合工厂方法of()

直接创建不可变集合,本身就不能增删改:

List<String> list = List.of("a", "b");
Set<String> set = Set.of("a", "b");
Map<String, String> map = Map.of("k1", "v1", "k2", "v2");

3.使用Guava的ImmutableXXX(第三方)

ImmutableList<String> list = ImmutableList.of("a", "b");
ImmutableMap<String, String> map = ImmutableMap.of("k", "v");

要点

  • 这些都是集合不可修改,但集合里的对象本身还是可以改(如果对象可变)。
  • 想完全不可变,还要保证元素是不可变对象(如String、包装类)。

如何边遍历边移除Collection中的元素?

正确方式只有一种:使用迭代器Iterator的remove()方法

其他方式(普通for、增强for)都会抛出ConcurrentModificationException。

1.正确方式:Iterator迭代器

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();

while (it.hasNext()) {
    String s = it.next();
    if ("b".equals(s)) {
        // 必须用迭代器的 remove()
        it.remove();
    }
}

2.错误方式(必抛异常)

①增强for循环(foreach)

for (String s : list) {
    list.remove(s); // 抛 ConcurrentModificationException
}

②普通for循环(容易漏元素、下标越界)

for (int i = 0; i < list.size(); i++) {
    list.remove(i); // 下标错乱,漏删
}

3.JDK8+优雅写法:removeIf

list.removeIf(s -> "b".equals(s));

底层也是用迭代器,推荐使用。

Comparable和Comparator区别?

定位

  • Comparable:内部比较器让类自己实现接口,定义默认排序规则。
  • Comparator:外部比较器单独写一个比较器,定义临时/灵活排序规则,不改动原类。

接口位置与方法

Comparable

  • 包:java.lang.Comparable
  • 方法:int compareTo(T o)
  • 类自己实现:class User implements Comparable<User>

Comparator

  • 包:java.util.Comparator
  • 方法:int compare(T o1, T o2)
  • 通常以匿名内部类/Lambda使用

使用方式

Comparable(自然排序)

Collections.sort(list, new Comparator<User>() {
    @Override
    public int compare(User u1, User u2) {
        return u2.getAge() - u1.getAge(); // 按年龄降序
    }
});

使用:

Collections.sort(list);

Comparator(定制排序)

Collections.sort(list, new Comparator<User>() {
    @Override
    public int compare(User u1, User u2) {
        return u2.getAge() - u1.getAge(); // 按年龄降序
    }
});

Lambda简化:

Collections.sort(list, (u1, u2) -> u2.getAge() - u1.getAge());

优先级

Comparator优先级高于Comparable,同时存在时,以Comparator为准。

优缺点对比

对比项 Comparable Comparator
类型 内部比较器 外部比较器
侵入性 强,必须修改实体类 无侵入,不改动原类
排序规则 只有一种默认排序 灵活,可定义多种排序
实现位置 实体类内部 外部单独实现

总结

Comparable是类自己实现的默认排序(compareTo);Comparator是外部定制排序(compare),灵活无侵入,优先级更高。

Collections工具类中的sort()方法如何比较元素?

比较方式

版本1:自然排序,依靠元素自身的Comparable接口

Collections.sort(list);

要求:元素必须实现Comparable接口

重写方法:compareTo(T o)

规则:

  • 返回负数:当前对象小→排在前面
  • 返回正数:当前对象大→排在后面
  • 返回0:相等

适用类:String、Integer、Long、Date等都已实现。

版本2:定制排序(比较器排序),传入Comparator比较器

Collections.sort(list, comparator);

不需要元素实现任何接口

重写方法:compare(T o1, T o2)

规则:

  • 返回负数:o1比o2小
  • 返回正数:o1比o2大
  • 返回0:相等

优先级Comparator>Comparable

排序算法

JDK6:MergeSort(归并排序)

JDK7+:

  • 对象集合:TimSort(优化版归并)
  • 基本类型数组:DualPivotQuickSort(双轴快排)

时间复杂度:O(nlogn)

总结

Collections.sort()有两种比较方式:

1.自然排序:元素实现Comparable接口,重写compareTo()

2.定制排序:传入Comparator比较器,重写compare()

比较器优先级高于自然排序。

List

List接口有什么特点?

  1. 继承自Collection:是单列有序集合的顶层接口。
  2. 元素存取有序:插入顺序与遍历顺序一致。
  3. 元素可重复:允许存储相同内容的对象。
  4. 带有索引:支持通过int类型下标对元素进行访问、插入、替换、删除操作。
  5. 提供特有遍历方式:支持普通for循环、ListIterator双向迭代器。
  6. 功能丰富:包含add、get、set、remove、indexOf、subList等索引相关方法。
  7. 常用实现类:ArrayList、LinkedList、Vector。
  8. 线程不安全:默认实现都非线程安全,多线程需使用并发包对应类。

遍历一个List有哪些不同的方式,每种方法的实现原理是什么,Java中List遍历的最佳实践是什么?

Java中遍历List的5种方式

1.普通for循环(通过索引)

for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
}

原理

  • 依靠索引下标,直接调用get(i)获取元素
  • ArrayList效率极高(数组随机访问)
  • LinkedList效率极低(链表每次都从头遍历找节点)

2.增强for循环(foreach)

for (String s : list) {
}

原理

  • 底层是Iterator迭代器的语法糖
  • 编译后自动变成迭代器代码
  • 简洁、易用,但不能增删元素

3.Iterator迭代器

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
}

原理

  • 统一的集合遍历方式,不依赖索引
  • 底层用modCount检测并发修改
  • 唯一支持遍历中删除的方式

4.ListIterator双向迭代器

ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    String s = lit.next();
}

原理

  • Iterator的子接口,支持向前/向后遍历
  • 支持遍历中add、set、remove操作
  • 仅List集合有

5.JDK8+forEach+Lambda

list.forEach(s -> { ... });

原理

  • 基于函数式接口,内部用迭代器实现
  • 代码极简,适合简单遍历

5种方式对比

方式 优点 缺点 适用场景
普通for 最快、可操作索引 代码繁琐,LinkedList极慢 ArrayList随机访问
增强for 简洁易读 不能增删,无法获取索引 简单遍历
Iterator 可删除、通用 代码稍多 遍历中删除元素
ListIterator 双向遍历、增删改 仅List支持 需要增删改、双向遍历
Lambda+forEach 极简、优雅 无法控制流程(break/continue) JDK8+简单遍历

最佳实践

1.通用首选:增强for循环

  • 最简洁、可读性最高
  • 适合绝大多数只读取、不修改的场景

2.ArrayList追求性能:普通for

  • 数组结构,随机访问极快
  • 大数据量优先使用

3.LinkedList任何情况:迭代器/增强for

  • 绝对不要用普通for,性能极差

4.遍历中要删除:Iterator/removeIf

  • 唯一安全方式

5.JDK8+:LambdaforEach

  • 简单遍历优先使用,代码最优雅

ArrayList的优缺点?

优点

  1. 底层是动态数组,支持随机访问,通过索引get(i)速度极快,时间复杂度O(1)。
  2. 遍历速度快,数组内存连续,对CPU缓存友好。
  3. 尾部添加元素效率高,不需要移动元素。
  4. 使用简单、通用性强,是日常开发最常用的集合。
  5. 按索引查找、更新元素非常方便。

缺点

1.插入、删除效率低

非尾部插入/删除需要移动后续元素,时间复杂度O(n)。

2.扩容有开销

满了会按1.5倍扩容,复制数组,耗时且可能浪费内存。

3.内存连续要求高

大数据量时可能难以找到大块连续内存空间。

4.线程不安全

多线程并发修改会出现数据错乱或并发修改异常。

5.只能存引用类型

不能直接存基本数据类型。

如何实现数组和List之间的转换?

数组→List

1.Arrays.asList()(最常用)

String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);

⚠️注意

  • 返回的是Arrays内部类的List,不是ArrayList
  • 不能增删,调用add/remove会抛UnsupportedOperationException
  • 数组和List共用同一内存,改数组会影响List

2.真正转成ArrayList(推荐)

List<String> list = new ArrayList<>(Arrays.asList(arr));

3.JDK8+Stream方式

List<String> list = Arrays.stream(arr).collect(Collectors.toList());

List→数组

1.toArray()方法(推荐带泛型)

List<String> list = new ArrayList<>();
// 方式1:带类型(正确)
String[] arr = list.toArray(new String[0]);

// 方式2:无类型,需要强转(不推荐)
String[] arr = (String[]) list.toArray();

ArrayList和LinkedList的区别是什么?

1.底层数据结构不同

  • ArrayList:动态数组
  • LinkedList:双向链表(JDK7+非循环)

2.访问(查询)效率不同

  • ArrayList:支持随机访问,get(i)时间复杂度O(1),查询快
  • LinkedList:不支持随机访问,查找需要从头遍历,复杂度O(n),查询慢

3.增删效率不同

ArrayList

  • 尾部增删快
  • 中间/头部增删需要移动元素,慢

LinkedList

  • 已知节点时,增删只改引用,极快
  • 但查找节点仍需遍历,整体不一定更快

4.内存占用不同

  • ArrayList:内存连续,占用空间少,只是扩容时可能有冗余
  • LinkedList:每个节点除数据外,还要存prev、next指针,内存开销更大

5.支持功能不同

  • ArrayList:只实现List接口
  • LinkedList:同时实现List、Deque接口,可做队列、双端队列、栈

6.线程安全

两者都线程不安全,多线程需使用CopyOnWriteArrayList或加锁

总结

ArrayList底层是数组,查询快、增删慢、内存紧凑;

LinkedList底层是双向链表,查询慢、增删快、内存开销大。

ArrayList和Vector的区别是什么?

1.线程安全

  • ArrayList线程不安全,方法没有加synchronized
  • Vector线程安全,所有方法都加了synchronized

2.效率

  • ArrayList:性能高,适合单线程
  • Vector:性能低,高并发场景锁竞争严重

3.扩容机制

  • ArrayList:默认扩容为1.5倍
  • Vector:默认扩容为2倍(也可手动指定扩容增量)

4.使用场景

  • ArrayList:日常开发首选
  • Vector:基本废弃,多线程用CopyOnWriteArrayList

5.支持的迭代器

  • ArrayList支持fast-fail迭代器
  • Vector也支持,但两者都不适合高并发

总结

ArrayList线程不安全、效率高、扩容1.5倍;

Vector线程安全、效率低、扩容2倍,现在基本不用。

ArrayList扩容机制?

1.底层结构

ArrayList底层基于Object[]数组实现,容量就是数组的长度。

2.初始化逻辑

  • 使用无参构造newArrayList<>()时,初始数组为空数组{},并不会立即分配容量。
  • 第一次调用add()添加元素时,才会将数组扩容并初始化为默认容量10。

3.扩容触发条件

每次执行add/addAll时,会先判断:当前元素个数+新增元素个数>数组长度满足则触发扩容。

4.扩容计算规则

新容量计算公式:新容量=旧容量+旧容量>>1等价于扩容为原容量的1.5倍。

如果计算后的新容量仍不满足需求,则直接扩容到所需的最小容量。

5.扩容实现方式

通过Arrays.copyOf()方法创建一个新的更大数组,并将原数组中的所有元素复制到新数组中,最后将底层数组引用指向新数组。

6.性能与优化

  • 扩容涉及数组复制,属于耗时操作,频繁扩容会降低效率。
  • 若能预估数据量,建议在创建时指定初始容量,避免多次自动扩容。

插入数据时,ArrayList、LinkedList、Vector谁速度较快,阐述ArrayList、Vector、LinkedList的存储性能和特性?

1.插入速度对比

  • 头部/中间插入:LinkedList最快,ArrayList、Vector较慢(需要移动数组元素)
  • 尾部插入:ArrayList≈LinkedList略快于Vector(Vector有synchronized锁开销)
  • 批量插入:ArrayList/Vector一次性扩容后复制,通常比LinkedList快

2.存储性能与特性完整总结

ArrayList

  • 底层结构:动态Object数组
  • 访问特性:支持随机访问,get(i)时间复杂度O(1),查询极快
  • 插入删除
    • 尾部增删快
    • 头部/中间增删需要移动后续元素,效率低O(n)
  • 扩容机制:默认容量10,满后扩容为原容量的1.5倍
  • 内存:内存连续、占用小,无额外指针开销
  • 线程安全非线程安全
  • 适用场景:读多写少、频繁查询、尾部增删为主

Vector

  • 底层结构:动态Object数组(与ArrayList几乎一致)
  • 访问特性:支持随机访问,get(i)O(1)
  • 插入删除:效率同ArrayList,但方法加锁更慢
  • 扩容机制:默认容量10,满后扩容为原容量的2倍
  • 内存:内存连续,扩容倍数更大,内存浪费相对更多
  • 线程安全线程安全(方法上加synchronized)
  • 性能:锁竞争严重,并发性能差
  • 适用场景:基本废弃,多线程场景优先用CopyOnWriteArrayList

LinkedList

  • 底层结构双向链表
  • 访问特性:不支持随机访问,查找需遍历,O(n)
  • 插入删除
    • 已知节点时增删仅修改引用,效率极高O(1)
    • 查找节点仍需遍历,实际不一定更快
  • 扩容机制:无扩容,按需创建节点
  • 内存:每个节点存data+prev+next,内存开销大
  • 线程安全非线程安全
  • 适用场景:频繁头尾增删、用作队列/栈/双端队列

3.核心总结

  • 查询速度:ArrayList≈Vector>LinkedList
  • 头部/中间插入:LinkedList最快
  • 内存效率:ArrayList>Vector>LinkedList
  • 线程安全:Vector安全,另两个不安全
  • 日常开发首选:ArrayList

多线程场景下如何使用ArrayList?

1.使用Collections.synchronizedList包装

List<String> safeList = Collections.synchronizedList(new ArrayList<>());
  • 原理:对每个方法加synchronized
  • 优点:简单、兼容原有逻辑
  • 缺点:锁粒度大,高并发下性能一般

2.使用CopyOnWriteArrayList(推荐)

List<String> safeList = new CopyOnWriteArrayList<>();
  • 属于JUC并发包,读无锁、写时复制新数组
  • 适合:读多写少的高并发场景
  • 优点:并发性能远好于synchronized方案
  • 缺点:写开销大,数据弱一致性

3.手动加锁(synchronized/Lock)

synchronized (lock) {
    arrayList.add(xxx);
}
  • 灵活控制锁范围,适合需要复合操作的场景
  • 缺点:代码侵入性强,容易出错

4.使用线程安全的替代集合

  • 高并发读:CopyOnWriteArrayList
  • 需要强一致性、高并发写:考虑用队列或其他并发结构

对比与选择

方案 优点 缺点 适用场景
Collections.synchronizedList 简单、兼容ArrayList 并发性能差 低并发、简单同步需求
CopyOnWriteArrayList 读性能极高、线程安全 写开销大、弱一致性 读多写少、高并发
手动加锁 灵活控制锁粒度 代码复杂、易出错 复杂复合操作

三、最佳实践

  • 低并发、简单场景:用Collections.synchronizedList
  • 高并发、读多写少:优先使用CopyOnWriteArrayList
  • 不建议在多线程直接使用原生ArrayList,会出现线程安全问题

为什么ArrayList的elementData加上transient修饰?

1.明确两个关键点

elementData是ArrayList底层存储数据的数组:

transient Object[] elementData;

transient关键字作用:被修饰的成员变量不会被Java默认序列化机制序列化。

2.为什么要加transient?

核心原因:节省空间+避免序列化无用数据

ArrayList的elementData是一个动态扩容数组,它的长度(容量)通常大于实际元素个数(size)。

  • 数组里很多位置是null(未使用的空间)
  • 如果不使用transient,序列化时会把整个数组+所有null值都写出去
  • 导致序列化体积变大、速度变慢、浪费大量IO

设计目的:只序列化真正有数据的元素,不序列化空位置。

3.元素如何序列化?

ArrayList自己重写了:

  • writeObject()
  • readObject()

序列化流程

  1. elementData被transient修饰→不参与默认序列化
  2. 序列化时,只遍历size个有效元素写入流
  3. 反序列化时,再把元素读回来重建数组

这样既安全又高效,只存有用数据。

Set

Set接口有什么特点?

1.继承自Collection,属于单列集合,存储单个元素。

2.不允许元素重复:保证存储的元素唯一性,通过hashCode()和equals()共同判断。

3.大部分实现类无序:存入顺序和取出顺序不保证一致(LinkedHashSet除外)。

4.无索引:不提供通过下标访问元素的方法,不能使用普通for循环遍历。

5.最多存一个null:只允许存入一个null元素,避免重复。

6.遍历方式:只能通过增强for、迭代器、forEach遍历。

7.常用实现类

  • HashSet:底层HashMap,无序,性能最高
  • LinkedHashSet:底层LinkedHashMap,保证插入顺序
  • TreeSet:底层TreeMap,可自然排序或定制排序

8.线程不安全:默认实现均非线程安全,多线程用CopyOnWriteArraySet或ConcurrentSkipListSet。

HashSet的实现原理?

1.底层依赖

HashSet底层实际是HashMap,HashSet只是在使用HashMap的key存储元素,value统一存一个固定的空对象。

// HashSet 源码
private transient HashMap<E,Object> map;
// 所有 key 对应的 value 都是这个静态空对象
private static final Object PRESENT = new Object();

2.存储结构

  • JDK7:哈希表(数组+链表)
  • JDK8+:哈希表(数组+链表/红黑树)

3.添加元素原理(add方法)

1.调用add(e)时,实际执行:

map.put(e, PRESENT);

2.根据元素的hashCode()计算哈希值,确定数组下标位置。

3.如果该位置没有元素,直接存入。

4.如果该位置已有元素,通过equals()比较是否相同:

  • 相同:视为重复元素,不存入,返回false。
  • 不同:挂在链表后面,链表长度≥8转为红黑树。

5.存入成功返回true。

4.唯一性保证机制

元素不重复依靠两个方法:

  • hashCode():定位存储位置
  • equals():判断是否为同一个元素

要让自定义对象能正确去重,必须同时重写hashCode和equals。

5.基本特性

  • 无序:不保证插入顺序与遍历顺序一致
  • 无索引:不能通过下标访问
  • 不可重复
  • 允许存一个null
  • 线程不安全
  • 查找、添加、删除平均时间复杂度O(1)

6.扩容机制

完全继承HashMap的扩容规则:

  1. 默认初始容量16,负载因子0.75
  2. 元素个数达到容量×负载因子时,扩容为原来2倍
  3. 重新哈希,重新分配所有元素位置

HashSet、LinkedHashSet、TreeSet区别?

1.底层结构

  • HashSet:底层是HashMap(数组+链表/红黑树)
  • LinkedHashSet:底层是LinkedHashMap(HashMap+双向链表)
  • TreeSet:底层是TreeMap(红黑树)

2.顺序性

  • HashSet:无序,不保证插入顺序,也不保证自然顺序
  • LinkedHashSet:有序,按照元素插入顺序遍历
  • TreeSet:有序,按照元素的自然排序(Comparable)或定制排序(Comparator)遍历

3.元素要求

  • HashSet/LinkedHashSet:元素只需重写hashCode()+equals()保证去重
  • TreeSet:元素必须可比较,要么实现Comparable接口,要么传入Comparator比较器,否则会报ClassCastException

4.是否允许null

  • HashSet、LinkedHashSet:允许存入一个null
  • TreeSet:不允许null(比较时会抛空指针异常)

5.性能

  1. HashSet:查询、增删最快,O(1)
  2. LinkedHashSet:略慢于HashSet,因为要维护双向链表
  3. TreeSet:性能最低,O(logn),红黑树排序开销大

6.线程安全

三者均线程不安全,多线程用CopyOnWriteArraySet或ConcurrentSkipListSet

7.使用场景

  1. HashSet:只需要去重、不关心顺序,追求最高性能
  2. LinkedHashSet:需要去重+保持插入顺序
  3. TreeSet:需要去重+自动排序(升序/降序)

TreeSet如何排序?

底层原理

TreeSet底层是TreeMap,基于红黑树实现,会自动对所有元素进行排序,保证遍历出来的结果是有序的。

排序方式

TreeSet支持自然排序和定制排序,必须二选一,否则会抛异常。

1.自然排序(默认方式)

要求:元素类必须实现Comparable接口,重写compareTo()方法。

实现步骤

  1. 自定义类实现Comparable<T>
  2. 重写compareTo方法指定比较规则
public class User implements Comparable<User> {
    private String name;
    private int age;

    // 按年龄升序
    @Override
    public int compareTo(User o) {
        return this.age - o.age; 
    }
}

排序规则

  • 返回负数:当前对象,放左边
  • 返回正数:当前对象,放右边
  • 返回0:视为重复元素,不存入

2.定制排序(灵活排序)

不需要修改元素类,在创建TreeSet时传入Comparator比较器。

TreeSet<User> set = new TreeSet<>(new Comparator<User>() {
    @Override
    public int compare(User u1, User u2) {
        // 按年龄降序
        return u2.getAge() - u1.getAge();
    }
});

优先级:Comparator定制排序>Comparable自然排序

TreeSet如何保证有序+唯一

  1. 不使用equals()、hashCode()
  2. 完全依靠比较器(compareTo/compare)
  3. 返回0就判定为重复元素,不会存入

特点总结

  • 有序:自动升序/降序排列
  • 唯一:根据比较结果去重
  • 不允许null:比较会抛空指针
  • 底层红黑树,增删改查效率O(logn)
  • 必须可比较,否则无法使用

Map

Map接口有什么特点?

1.顶层独立接口,不属于Collection体系,和Collection平级。

2.存储键值对(key-value),一个key对应一个value。

3.key不允许重复,重复key会覆盖旧value;value可以重复。

4.key最多允许一个null,value可以有多个null。

5.无序:大部分实现不保证插入顺序,也不保证排序(LinkedHashMap、TreeMap除外)。

6.没有索引,不能通过下标访问,只能通过key操作数据。

7.存取方式:

  • 存:put(key,value)
  • 取:get(key)
  • 删:remove(key)

8.遍历方式:遍历keySet、遍历values、遍历entrySet。

9.线程不安全:常用实现类(HashMap、LinkedHashMap、TreeMap)均非线程安全,多线程用ConcurrentHashMap。

10.常用实现类:

  • HashMap:无序、性能最高
  • LinkedHashMap:保持插入顺序
  • TreeMap:按键自动排序
  • Hashtable:线程安全、性能低

HashMap的实现原理?

一、底层结构

  • JDK7:数组+链表
  • JDK8:数组+链表+红黑树
  • 数组是主体,称为哈希表/桶数组,链表用于解决哈希冲突,红黑树用于提高冲突过多时的查询效率。

二、存储过程(put流程)

1.根据key的hashCode()计算哈希值,再做扰动处理,得到最终哈希。

2.通过(数组长度-1)&hash计算数组下标,定位到对应桶位置。

3.如果该位置为空,直接将节点存入。

4.如果该位置已有元素:

  1. 先比较key是否相同(==或equals),相同则覆盖value。
  2. 不同则形成链表,JDK8采用尾插法。

5.链表长度达到8且数组长度≥64时,链表转为红黑树。

6.红黑树节点数减少到6时,会退化为链表。

7.元素数量达到容量×负载因子(0.75)时,触发扩容。

三、核心参数

  • 默认初始容量:16
  • 负载因子:0.75
  • 扩容阈值:容量×0.75
  • 扩容倍数:每次扩容为原来的2倍
  • 树化阈值:链表长度≥8
  • 退化阈值:红黑树节点≤6

四、哈希冲突解决

  • 采用链地址法:相同下标位置用链表连接。
  • JDK8加入红黑树优化,避免长链表导致查询效率退化到O(n)。

五、扩容机制

  1. 创建新数组,长度为原数组2倍。
  2. 重新计算所有元素的下标。
  3. 元素要么在原位置,要么移动到原下标+原容量的位置。
  4. JDK8扩容时保持链表相对顺序,避免倒置,减少并发问题。

六、key相关规则

  • 允许key为null,固定存在下标0位置。
  • 唯一性依靠:hashCode()+equals()。
  • 自定义对象作为key必须同时重写这两个方法,否则无法正确存取。

七、线程安全

  • HashMap线程不安全。
  • 多线程环境下推荐使用ConcurrentHashMap。

八、时间复杂度

  • 正常情况:增删改查O(1)
  • 哈希冲突严重:链表O(n),红黑树O(logn)

HashMap在JDK1.7和JDK1.8中有哪些不同?

1.底层数据结构不同

  • JDK1.7:数组+链表
  • JDK1.8:数组+链表+红黑树链表长度≥8且数组长度≥64时转为红黑树,节点≤6时退化为链表。

2.哈希冲突时链表插入方式不同

  • JDK1.7:头插法(新元素放在数组位置,旧节点接在后面)高并发扩容下可能形成环形链表,导致死循环。
  • JDK1.8:尾插法(新节点加到链表末尾)扩容时保持顺序,不会形成环形链表。

3.哈希扰动算法不同

  • JDK1.7:进行4次位移+异或扰动,计算复杂。
  • JDK1.8:简化为1次高16位异或低16位hash=key.hashCode()^(key.hashCode()>>>16),效率更高,散列更均匀。

4.扩容后数据存储位置计算

  1. JDK1.7:重新全部哈希计算索引。
  2. JDK1.8:按原索引或原索引+旧容量两种位置,只需判断新bit位,不用重新全哈希,效率更高。

5.效率表现

  • JDK1.7:冲突严重时链表过长,查询退化为O(n)。
  • JDK1.8:使用红黑树,最坏情况O(logn),查询效率大幅提升。

6.线程安全隐患

  • JDK1.7:多线程扩容头插法会链表成环,CPU100%。
  • JDK1.8:解决了环形链表问题,但仍非线程安全,多线程仍要用ConcurrentHashMap。

    能否使用任何类作为Map的key?

    理论上可以用任意类作为Map的key,但能不能正常工作,取决于这个类是否遵守了key的设计规范。

    一、能不能用?

    • 语法上:任何类都能作为Map的key(因为key类型是Object)。
    • 功能上:不是所有类都适合,用不好会出现数据找不到、内存泄漏、无法去重等问题。

    二、作为Map的key必须满足什么条件?

    以最常用的HashMap为例:

    1.必须正确重写hashCode()和equals()

    存入时:根据hashCode()计算位置

    比较重复时:用equals()判断是否是同一个key

    如果不重写:

    • hashCode()默认用对象地址计算
    • equals()默认用==判断→new出来的两个内容相同的对象,会被当成不同key,数据存了找不到。

    2.建议类是不可变类(Immutable)

    比如String、Integer、Long都是不可变的。

    如果对象是可变的:

    • 存入后修改属性→hashCode()变了
    • 导致该key所在数组下标“漂移”→这个key-value就永远丢失、无法获取也无法删除。

    3.不能直接用数组作为key

    数组默认没有重写hashCode()和equals(),会直接按对象地址判断,极其容易出问题。

    三、哪些类非常适合做key?

    String(最常用)

    Integer、Long、Double等包装类

    自定义不可变类:

    • final类
    • 成员变量privatefinal
    • 只提供getter,不提供setter
    • 正确重写hashCode()+equals()

    四、总结

    1. 语法上可以用任何类作为Map的key,但不是所有类都适合。
    2. 要保证正常存取,必须重写hashCode()和equals(),保证相等对象有相同哈希码。
    3. 强烈建议使用不可变类,避免key状态改变导致哈希值变化,使键值对“丢失”。
    4. 实际开发优先使用String、Integer等标准不可变类。

    HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

    1.hashCode返回值范围太大,数组装不下

    • Object.hashCode()返回的是int类型,范围是:-2³¹~2³¹-1,大约40亿的范围。

    • 而HashMap底层数组初始容量只有16,后面扩容也是32、64、128……

    • 直接用hashCode当下标,绝大多数位置都会越界,根本无法存入数组。

    2.直接使用会导致大量下标越界

    数组下标必须是[0,length-1]之间的整数。hashCode可能是负数、也可能远大于数组长度,不能直接用。

    3.哈希值分布不够均匀,容易造成大量冲突

    • 很多对象的hashCode()实现质量一般,高位变化大、低位变化小。
    • 如果只简单取余,冲突会非常多,链表变长,效率下降。

    所以HashMap做了两步优化:

    1. 扰动函数:hash=key.hashCode()^(hash>>>16),让高16位也参与运算,让哈希结果更分散。
    2. 下标计算:index=hash&(table.length-1),等价于取模,但位运算更快,且保证下标在合法范围内。

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

    1.让下标计算更高效,用&代替%取模

    • 计算数组下标公式:index = hash & (length - 1)
    • 当且仅当length是2的幂时:hash % length == hash & (length - 1)
    • 位运算&比取模运算%快得多,能大幅提升性能。

    2.保证下标分布均匀,减少哈希冲突

    • 2的幂对应的二进制是100…0,length-1就是011…1
    • 做&运算时,hash的低位会被完整保留,分布更均匀
    • 如果不是2的幂,length-1二进制末尾会出现0,会导致部分数组位置永远用不到,冲突加剧

    3.扩容时重新计算下标更简单高效

    • 扩容为原来2倍(仍是2的幂)
    • 元素新下标只有两种可能:
      • 原下标位置不变
      • 新下标=原下标+原容量
    • 只需判断hash新增的那个高位是0还是1即可,不用重新全量哈希,提升扩容效率

    4.方便扩容与移位操作

    • 扩容始终是<<1移位操作,简单高效
    • 保证新数组长度依旧是2的幂,维持上述所有优点

    HashMap、HashTable、TreeMap、LinkedHashMap特点与区别?

    一、底层结构

    • HashMap:数组+链表+红黑树(JDK8)
    • Hashtable:数组+链表
    • TreeMap:红黑树
    • LinkedHashMap:HashMap+双向链表(继承HashMap)

    二、顺序性

    • HashMap:无序,不保证插入顺序
    • Hashtable:无序
    • TreeMap:按键排序(自然排序,比较器排序)
    • LinkedHashMap:保持插入顺序

    三、是否允许null

    • HashMap:key允许1个null,value允许多个null
    • Hashtable:key和value都不允许null,否则NPE
    • TreeMap:key不允许null,value可以null
    • LinkedHashMap:同HashMap,key最多一个null

    四、线程安全

    • HashMap:非线程安全
    • Hashtable:线程安全(方法上加synchronized)
    • TreeMap:非线程安全
    • LinkedHashMap:非线程安全

    五、性能

    • HashMap:高,无锁开销
    • Hashtable:低,锁粒度大,并发差
    • TreeMap:较低,O(logn),需排序
    • LinkedHashMap:略低于HashMap,需维护链表

    六、key要求

    • HashMap、Hashtable、LinkedHashMap:key重写hashCode()+equals()
    • TreeMap:key必须可比较(实现Comparable或传入Comparator)

    七、扩容机制

    • HashMap:默认16,负载因子0.75,扩容2倍
    • Hashtable:默认11,负载因子0.75,扩容2倍+1
    • TreeMap:无数组,无需扩容
    • LinkedHashMap:同HashMap

    八、适用场景

    • HashMap:通用,不需要顺序,追求性能
    • Hashtable:基本废弃,高并发用ConcurrentHashMap
    • TreeMap:需要按键排序、范围查找
    • LinkedHashMap:需要去重+保持插入顺序,或实现LRU

    对比表

    特性 HashMap Hashtable TreeMap LinkedHashMap
    结构 数组+链表+红黑树 数组+链表 红黑树 HashMap+双向链表
    顺序 无序 无序 按键排序 插入有序
    null允许 key1个,value多个 都不允许 key不允许 同HashMap
    线程安全 是(synchronized)
    性能 中(O(logn)) 略低于HashMap
    适用场景 绝大多数场景 已废弃 排序、范围查找 保序、LRU缓存

    总结

    • 日常开发优先用HashMap
    • 要插入有序用LinkedHashMap
    • 要自动排序用TreeMap
    • Hashtable直接抛弃,高并发用ConcurrentHashMap

    HashMap线程不安全表现?

    1.数据覆盖(最常见)

    • 两个线程同时使用相同下标插入数据。
    • 线程A判断位置为空,准备插入;
    • 此时线程B也判断为空,也插入;
    • 结果:一个数据被另一个覆盖,数据丢失。

    2.扩容导致死循环(JDK1.7经典问题)

    • JDK1.7使用头插法,多线程扩容会让链表形成环形链。
    • 一旦触发环形链表,get()时会无限循环遍历,导致CPU100%,服务卡死。
    • JDK1.8改用尾插法修复了死循环,但依旧线程不安全。

    3.扩容导致数据丢失

    • 多线程同时扩容,会互相覆盖重新哈希后的节点。
    • 结果:部分数据直接丢失,无法找回。

    4.size统计不准确

    • size没有原子性保障。
    • 多个线程同时put,size++会出现计数少算的情况。

    总结

    HashMap线程不安全主要表现为:多线程put出现数据覆盖、数据丢失;JDK1.7扩容会产生环形链表导致死循环;size计数不准确。

    解决方案

    绝对不要在多线程中使用HashMap!

    • 高并发:用ConcurrentHashMap(推荐)
    • 低并发:用Collections.synchronizedMap

    TreeMap如何排序?

    底层原理

    TreeMap是按键排序的Map,底层基于红黑树实现,排序规则由Key决定,有两种排序方式:

    排序方式

    1.自然排序(默认)

    要求

    Key对应的类必须实现Comparable接口,重写compareTo()方法。

    常用类型默认规则

    • Integer、Long:升序
    • String:按字母字典序排序
    • Date:按时间先后

    示例

    TreeMap<Integer, String> map = new TreeMap<>();
    map.put(3, "C");
    map.put(1, "A");
    map.put(2, "B");
    // 输出:1,2,3 升序

    2.定制排序(自定义规则)

    创建TreeMap时,传入Comparator比较器,Key不需要实现Comparable。

    TreeMap<Integer, String> map = new TreeMap<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            // 降序排列
            return o2 - o1;
        }
    });

    排序与去重规则

    1. TreeMap不使用equals和hashCode
    2. 完全依靠compare/compareTo判断是否重复
    3. 返回0→视为同一个Key,覆盖旧值

    特点

    1. 按键有序,不是插入顺序
    2. Key不能为null(比较会抛NPE)
    3. Value可以为null
    4. 增删改查时间复杂度O(logn)

    HashSet与HashMap的区别?

    核心关系

    • HashSet底层就是HashMap,是对HashMap的简单包装。
    • HashSet只用到HashMap的key,value统一存一个固定空对象PRESENT。

    详细区别

    1.实现接口不同

    • HashSet:实现Set接口,单列集合
    • HashMap:实现Map接口,双列集合

    2.存储内容不同

    • HashSet:只存单个对象
    • HashMap:存key-value键值对

    3.存入数据的方法

    • HashSet:add(Object o)底层调用map.put(o,PRESENT)
    • HashMap:put(key,value)

    4.重复判断

    • HashSet:通过元素的hashCode()+equals()保证不重复
    • HashMap:通过key的hashCode()+equals()保证key不重复

    5.效率

    • 两者基本一致,HashSet略轻量一点,但几乎无差别。

    6.是否允许null

    • HashSet:允许一个null
    • HashMap:key允许一个null,value允许多个null

    7.用途

    • HashSet:用于去重
    • HashMap:用于快速查找(通过key找value)

    总结

    • HashMap存键值对,用于映射关系查询;
    • HashSet存单个对象,用于去重;
    • HashSet底层就是HashMap,只是把元素当作key,value用固定空对象占位。

    Queue

    Queue接口有什么特点?

    1.Queue是Collection的直接子接口,与List、Set平级。

    2.遵循先进先出FIFO:先入队的元素优先出队。

    3.只操作头尾:元素从队尾添加,从队头取出,不支持随机访问。

    4.方法分两类:

    • 失败抛异常:add()、remove()、element()
    • 失败返回特殊值:offer()、poll()、peek()

    5.主要用于任务排队、消息队列、生产者-消费者模型。

    6.子接口Deque是双端队列,可做队列也可做栈。

    Queue和Deque有什么区别?

    1.接口定义不同

    1. Queue:单端队列接口,只能尾进头出(FIFO)
    2. Deque:双端队列接口(Deque=DoubleEndedQueue),两端都可以进出

    2.操作方式不同

    • Queue:只支持
      • 队尾添加
      • 队头删除/查看
    • Deque:支持
      • 队头/队尾都能添加
      • 队头/队尾都能删除/查看→既可以当队列,也可以当栈使用

    3.方法体系不同

    • Queue方法:
      • offer()/add()
      • poll()/remove()
      • peek()/element()
    • Deque额外支持:
      • offerFirst()/offerLast()
      • pollFirst()/pollLast()
      • peekFirst()/peekLast()
      • 栈方法:push()、pop()、peek()

    4.用途区别

    • Queue:标准先进先出队列
    • Deque
      • 可做队列(FIFO)
      • 可做栈(LIFO,官方推荐替代Stack)
      • 可做滑动窗口、工作窃取等

    5.继承关系

    • Deque继承自Queue
    • 所以Deque拥有Queue的所有功能,且功能更多

    总结

    1. Queue是单端队列,只能一头进一头出
    2. Deque是双端队列,两端都能进出,功能更强
    3. 日常开发优先用ArrayDeque实现Deque,性能最好。

    Queue中add()、offer()、remove()、poll()、element()、peek()的区别?

    操作逻辑完全一样,区别只在于:队列满/空时,一个抛异常,一个返回特殊值(false/null)。

    操作 抛异常的方法 返回特殊值的方法 说明
    添加元素

    add(e)

    offer(e)

    队列满时:add抛异常,offer返回false

    删除队首

    remove()

    poll()

    队列为空时:remove抛异常,poll返回null

    查看队首

    element()

    peek()

    队列为空时:element抛异常,peek返回null

    • add/remove/element:暴力版,出错直接抛异常
    • add/remove/element:暴力版,出错直接抛异常

    详细说明

    1.添加元素

    • add(e):队列满了抛出IllegalStateException
    • offer(e):队列满了返回false,不抛异常

    2.删除并返回队首

    • remove():队空抛出NoSuchElementException
    • poll():队空返回null

    3.获取但不删除队首

    • element():队空抛出NoSuchElementException
    • peek():队空返回null

    使用场景

    1. 业务允许队列空/满,需要安全处理→用offer/poll/peek
    2. 队列不应该空/满,出错就是bug,需要快速失败→用add/remove/element

    常见Queue实现类有哪些?

    一、普通队列(非线程安全)

    1.LinkedList

    • 实现了Queue和Deque接口
    • 底层双向链表
    • 允许存null

    2.ArrayDeque

    • 实现Deque,数组结构
    • 效率比LinkedList更高
    • 不允许null

    3.PriorityQueue

    • 优先级队列,底层堆(小顶堆)
    • 不按FIFO,按优先级出队
    • 不允许null,线程不安全

    二、线程安全队列

    1.ConcurrentLinkedQueue

    • 无界、无锁、线程安全
    • 基于CAS实现,高并发性能好

    2.ArrayBlockingQueue

    • 有界阻塞队列,数组实现
    • 满了阻塞put(),空了阻塞take()

    3.LinkedBlockingQueue

    • 可选有界/无界阻塞队列
    • 链表实现,吞吐量较高

    4.PriorityBlockingQueue

    • 支持优先级的阻塞队列

    5.SynchronousQueue

    • 不存储元素,直接移交
    • 一个插入必须等待一个删除

    PriorityQueue实现原理?

    1.底层结构

    • 底层是动态数组,实现了小顶堆(默认)结构
    • 堆是一棵完全二叉树,满足:父节点≤子节点(最小堆)

    2.排序规则

    • 元素必须可比较,两种方式:
      • 元素实现Comparable接口(自然排序)
      • 构造时传入Comparator比较器(定制排序)
    • 不满足则抛出ClassCastException

    3.核心操作原理

    • 入队offer()
    1. 元素添加到数组末尾
    2. 向上调整(siftUp):与父节点比较,交换位置,维持堆结构
    • 出队poll()
    1. 取出堆顶(下标0)元素
    2. 末尾元素移到堆顶
    3. 向下调整(siftDown):与子节点比较,交换位置,维持堆结构

    4.特点

    • 并非严格FIFO,而是按优先级出队
    • 线程不安全
    • 不允许null
    • 增删复杂度O(logn)
    • 默认容量11,满了自动扩容

    5.与普通Queue区别

    • Queue:先进先出
    • PriorityQueue:按优先级出队,与插入顺序无关

    为什么不推荐使用Stack?推荐用什么?

    1.继承设计不合理

    • Stack继承自Vector,而Vector是线程安全的动态数组。
    • 栈只需要后进先出(LIFO),却继承了Vector所有public方法,可以通过add(index,e)、remove(index)随意插入、删除中间元素,破坏了栈的封装和行为规范。

    2.性能差

    • Vector所有方法都加了synchronized,单线程环境下完全没必要,开销大、效率低。

    3.API老旧

    • 属于Java早期遗留类,官方早已不推荐,没有现代化优化。

    推荐使用Deque接口及其实现类来实现栈。

    • 首选:ArrayDeque(数组实现,效率最高)
    • 备选:LinkedList(链表实现,也可)

    用法对应

    • 入栈:push(e)
    • 出栈:pop()
    • 查看栈顶:peek()

    完全满足栈的LIFO行为,且没有多余方法、性能更高、线程不安全但可控。

    ArrayDeque与LinkedList区别?

    1.底层结构不同

    • ArrayDeque动态循环数组
    • LinkedList双向链表

    2.访问与操作效率

    • ArrayDeque
      • 内存连续,缓存友好,读写效率更高
      • 头插、尾插都是O(1),性能稳定
    • LinkedList
      • 内存分散,节点开销大
      • 插入删除O(1),但需要先遍历找到节点,实际更慢

    3.是否支持null

    • ArrayDeque:不允许存入null
    • LinkedList:允许null

    4.内存占用

    • ArrayDeque:紧凑,内存开销小
    • LinkedList:每个元素都要存前后指针,开销大

    5.功能定位

    • 两者都实现了Queue、Deque,都能当队列、栈用
    • LinkedList额外实现了List,支持随机访问(get(index)),但效率很低

    6.使用选择

    • 单纯做队列、栈→优先用ArrayDeque(更快、更省内存)
    • 需要频繁在中间插入删除,或必须存null→用LinkedList

    总结

    ArrayDeque是数组,性能高、不允许null;LinkedList是链表,支持null、功能多但效率低。队列/栈首选ArrayDeque。

      Queue和List的区别?

      1.接口定位不同

      • Queue:队列接口,遵循FIFO(先进先出)
      • List:线性表接口,强调有序可重复、可随机访问

      2.操作方式不同

      • Queue:只在头尾操作,不支持按索引增删改查
      • List:支持任意位置增删改查,有get(index)、add(index,e)等

      3.用途场景不同

      • Queue:用于任务排队、消息队列、生产者-消费者
      • List:用于普通数据存储、遍历、随机查找

      4.方法体系不同

      • Queue:offer()、poll()、peek()等队列专用方法
      • List:get()、set()、indexOf()等索引相关方法

      5.部分实现类重叠

      • LinkedList同时实现了List和Queue,所以它既可以当列表也可以当队列。

      Queue是队列,头尾操作、FIFO;List是线性表,支持索引、随机访问。

      并发集合

      为什么要使用并发集合?

      使用并发集合,核心是为了在多线程环境下保证线程安全,同时兼顾性能与易用性,替代低效的同步容器。

      1.解决线程安全问题

      普通集合(如ArrayList、HashMap、HashSet)非线程安全,多线程同时读写会出现:

      • 数据丢失、覆盖
      • 并发修改异常ConcurrentModificationException
      • 数据不一致、业务错乱并发集合内部实现了安全的并发控制,可直接在多线程下使用。

      2.比synchronized同步容器性能高很多

      早期同步容器(Vector、Hashtable、Collections.synchronizedList):

      • 粗粒度锁,整个方法加锁
      • 并发高时锁竞争严重,吞吐量低

      并发集合(ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue):

      • 采用分段锁、CAS、无锁设计、读写分离
      • 并发能力强、锁粒度更细、高并发下性能提升明显

      3.提供更适合并发场景的API

      • 原子操作:putIfAbsent、removeIf等复合原子操作
      • 弱一致性迭代器,遍历过程不会抛并发修改异常
      • 支持阻塞、非阻塞队列,方便做生产者消费者模型

      4.降低开发成本

      不用自己手动加锁、释放锁、处理死锁,减少代码复杂度与出错概率。

      ConcurrentHashMap的实现机制?

      1.JDK7:分段锁(Segment)机制

      核心结构:Segment[]+HashEntry[]+链表。Segment继承ReentrantLock,默认16个Segment,每个Segment是独立哈希表。

      并发控制

      • 写操作:根据key哈希定位到对应Segment,只锁当前Segment,其他Segment可并行访问。
      • 读操作:无锁,依赖volatile修饰的HashEntry保证内存可见性。

      扩容:每个Segment独立扩容,扩容时仅锁当前Segment。

      优缺点:并发度高(默认16);但Segment数量固定,跨段操作(如size())需锁全表,性能受限。

      2.JDK8+:CAS+桶级synchronized机制(主流)

      核心结构:Node[]table(volatile)+链表+红黑树(链表≥8且数组≥64树化,≤6反树化)。Node的val、next用volatile修饰,保证可见性。

      并发控制(核心)

      • 无锁读:get()全程无锁,通过volatile保证读取最新值。
      • 写操作(put/remove)
      1. 定位桶下标,用CAS尝试无锁插入空桶;
      2. 桶非空时,只锁当前链表/红黑树的头节点(桶级锁),锁粒度极细;
      3. 冲突时自旋重试,失败再用synchronized加锁。
      • 扩容优化:多线程协同扩容(化整为零),线程发现扩容时会协助迁移数据,用ForwardingNode标记迁移完成的桶。

      size计算:无全局锁,通过CAS累加计数,弱一致性但性能极高。

      ConcurrentHashMap与HashMap和Hashtable的差异?

      特性 ConcurrentHashMap(JDK8+) HashMap Hashtable
      线程安全

      是(CAS+桶级synchronized)

      否(多线程易数据覆盖/丢失/死循环)

      是(方法全加synchronized,全局锁)

      锁粒度 桶级(链表/红黑树头节点) 无锁 全表锁(整个对象)
      底层结构 数组+链表+红黑树 数组+链表+红黑树(JDK8+) 数组+链表
      null支持 键/值均不允许null(抛NPE) 键允许1个null,值允许多个 键/值均不允许null(抛 NPE)
      性能 高并发最优(无锁读+细粒度写) 单线程最高(无锁开销) 极低(全局锁,并发阻塞)
      扩容机制 多线程协同迁移,效率高 单线程重哈希 单线程重哈希
      迭代器

      弱一致性(不抛ConcurrentModificationException)

      快速失败(抛异常) 快速失败(抛异常)
      默认容量 16 16 11
      负载因子 0.75 0.75 0.75
      适用场景 高并发多线程环境 单线程通用场景 基本废弃(低并发可用,不推荐)

      关键区别

      1.线程安全机制(核心差异)

      • Hashtable:全表synchronized,同一时间仅1个线程操作,并发能力极差。
      • HashMap:无任何锁,多线程并发put/扩容会出现数据覆盖、丢失、JDK7死循环。
      • ConcurrentHashMap
        • JDK7:分段锁,不同Segment并行;
        • JDK8+:无锁读+CAS无锁写+桶级锁,锁冲突概率极低,并发性能接近HashMap。

      2.null限制(设计原因)

      • ConcurrentHashMap/Hashtable:不允许null键/值,避免get(null)时无法区分“键不存在”还是“值为null”,保证并发场景下的语义清晰。并发场景下两次操作不是原子的,中间可能被其他线程修改,导致判断结果不可靠。
      • HashMap:单线程无歧义,允许null提升灵活性。

      3.扩容效率

      • HashMap/Hashtable:单线程全量重哈希,扩容时阻塞所有操作。
      • ConcurrentHashMap:多线程协助迁移,扩容期间新老数组共存,读写不阻塞,大幅提升扩容性能。

      总结

      • Hashtable:全局锁,性能差,已废弃;
      • HashMap:单线程高效,多线程不安全;
      • ConcurrentHashMap:JDK8+用CAS+桶级锁,无锁读、细粒度写,高并发下性能最优,是多线程场景的首选。

      ConcurrentHashMap是弱一致性的体现?

      ConcurrentHashMap是典型的弱一致性体现,主要体现在迭代器和size()等方法上,目的是在高并发下提升性能。

      弱一致性体现在哪里?

      1.迭代器是弱一致性的

      • 迭代器遍历过程中,其他线程仍可以修改Map(put/remove)
      • 不会抛出ConcurrentModificationException
      • 但迭代器不保证能读到最新数据,只能保证读到创建迭代器时已存在的数据,或部分新数据

      2.size()、isEmpty()等方法弱一致

      • 高并发下这些方法不做全局加锁,只做统计估算
      • 返回的是近似值,调用瞬间可能已被其他线程修改
      • 不保证绝对精确

      3.get读操作无锁

      • 读不加锁,能读到最近一次完成的写入,但不保证立即可见最新值
      • 属于弱一致,而非强一致

      为什么设计成弱一致性?

      • 如果要强一致性,就必须全局加锁
      • 会导致并发性能急剧下降,失去高并发优势
      • 弱一致性是性能与一致性的折中,满足绝大多数并发业务场景

      哪些操作是强一致?

      • put、get、remove、replace等单个原子操作是线程安全、强一致的
      • 但复合操作(如get+put)不保证原子性,仍需加锁或使用原子方法

      什么是CopyOnWriteArrayList?适用场景?

      是什么

      • 线程安全的List实现,位于java.util.concurrent包下
      • 底层采用写时复制(Copy-On-Write)机制
      • 读操作无锁,写操作通过复制新数组实现

      核心原理

      • 内部持有一个volatile数组,只保证可见性,不加锁
      • 读:直接读取原数组,不加锁,性能极高
      • 写(add/set/remove等)
        1. 加ReentrantLock锁,保证同一时刻只有一个线程写
        2. 复制出一个新数组,在新数组上做修改
        3. 完成后将引用指向新数组
        4. 旧数组等待GC回收
      • 读写分别操作不同数组,不存在竞争,所以读不用加锁

      特点

      • 读极快,写有开销
      • 迭代器遍历的是快照,不支持增删改,不会抛出ConcurrentModificationException
      • 弱一致性:写操作完成前,其他线程读到的仍是旧数据
      • 内存占用较高:写操作会临时存在新旧两个数组

      优点

      • 高并发读场景性能远超Vector、Collections.synchronizedList
      • 读操作无锁,无锁竞争
      • 遍历安全,适合边遍历边修改的并发场景

      优点

      • 写操作性能差,有数组复制开销
      • 内存占用大
      • 数据存在短暂不一致,不适合强一致性要求场景

      适用场景

      • 读多写少的高并发场景(白名单、配置列表、商品分类、缓存列表等)
      • 遍历操作远多于增删改操作
      • 允许读取到稍旧数据、对数据实时性要求不高
      • 需避免并发修改异常的遍历场景

      什么是阻塞队列BlockingQueue?

      BlockingQueue是java.util.concurrent包下的线程安全队列,在普通队列基础上增加了阻塞等待机制,主要用于生产者-消费者模型。

      核心阻塞特点

      • 队列满时:调用put()放入元素的线程会阻塞等待,直到队列有空闲位置
      • 队列空时:调用take()取出元素的线程会阻塞等待,直到队列有元素

      常用方法对比

      操作 抛异常 返回特殊值 阻塞等待 超时等待
      入队 add() offer() put() offer(e,timeout,unit)
      出队 remove() poll() take() poll(timeout,unit)

      常见实现类

      • ArrayBlockingQueue:数组实现,有界,公平/非公平
      • LinkedBlockingQueue:链表实现,可选有界/无界
      • PriorityBlockingQueue:优先级阻塞队列
      • SynchronousQueue:不存储元素,直接移交
      • DelayQueue:延迟队列

      应用场景

      • 生产者-消费者模式
      • 线程池任务队列
      • 流量削峰、异步解耦

      BlockingQueue=线程安全队列+满了put阻塞、空了take阻塞,专门解决多线程生产消费同步问题。

      ConcurrentLinkedQueue原理?和BlockingQueue的对比?

      1.结构

      • 无界、线程安全的单向链表队列
      • 实现Queue接口,不是BlockingQueue

      2.并发机制

      • 全程无锁,基于CAS+volatile实现线程安全
      • 入队、出队都使用CAS自旋重试,不阻塞线程

      3.特点

      • 高并发吞吐量极高,适合大量读写
      • 无容量限制,会一直扩容
      • 不支持阻塞(没有put()/take())
      • 不允许null
      对比项 ConcurrentLinkedQueue BlockingQueue(如ArrayBlockingQueue)
      实现方式 CAS+自旋,无锁 ReentrantLock+条件队列,有锁
      是否阻塞 不阻塞 满则put阻塞,空则take阻塞
      容量 无界 大多有界(可控制内存)
      适用场景 高并发、不希望阻塞 生产者-消费者、流量控制、线程池
      API offer/poll/peek 支持阻塞put/take+超时
      内存风险 无界可能OOM 有界更安全,可限流
      性能 极高(无锁) 较低(锁竞争)

      总结

      • ConcurrentLinkedQueue:无锁、无界、不阻塞,高并发性能最强,适合纯异步消费。
      • BlockingQueue:有锁、可阻塞、可限流,是生产者-消费者模型的标准方案。

      ConcurrentSkipListMap/ConcurrentSkipListSet特点?

      核心特点

      1.有序基于跳表(SkipList)实现,元素默认按key升序排列,可指定比较器自定义排序。

      2.并发安全采用CAS+细粒度锁实现并发控制,无全局锁,高并发下性能优于Collections.synchronizedSortedMap。

      3.高并发下比TreeMap更合适

      • TreeMap非线程安全
      • 加锁的TreeMap并发性能差
      • ConcurrentSkipListMap适合高并发+有序场景

      4.key不允许为null同ConcurrentHashMap,避免并发场景下的二义性。

      5.弱一致性迭代器遍历是快照级别的弱一致性,不会抛ConcurrentModificationException。

      6.支持范围查询自带headMap、tailMap、subMap等范围查找方法,是高并发场景下有序+范围查询的首选。

      适用场景

      • 需要高并发+有序的场景
      • 需按key范围查询、排序、分页
      • 秒杀、限流、定时任务、有序事件队列等

      总结

      ConcurrentSkipListMap/Set是高并发下的有序集合,基于跳表实现,线程安全、支持范围查询,并发性能远优于加锁的TreeMap。

      多线程遍历集合为什么会抛ConcurrentModificationException?

      多线程遍历集合时抛出ConcurrentModificationException,根本原因是:迭代器采用了快速失败(fail-fast)机制,检测到集合结构被并发修改,为避免数据错乱而主动抛异常。

      1.底层原理

      • 集合内部维护一个modCount(修改次数)
      • 迭代器创建时会保存一份expectedModCount=modCount
      • 每次迭代(next)都会对比两者是否相等
      • 若其他线程增删元素导致modCount改变
      • 迭代器检测到不一致,立即抛出异常

      2.为什么单线程也可能抛?

      单线程一边遍历一边remove/add也会抛,因为同样触发了modCount与expectedModCount不一致。

      3.本质目的

      不是为了判定线程安全,而是尽早暴露并发修改问题,避免遍历过程中出现数据丢失、重复、越界等不可预期错误。

      4.如何避免?

      • 使用并发集合(ConcurrentHashMap、CopyOnWriteArrayList),它们是弱一致性,不抛此异常
      • 使用迭代器自带的remove()而非集合的remove()
      • 遍历前加锁,保证串行执行

      总结

      因为普通集合迭代器是快速失败机制,遍历过程中一旦检测到集合被其他线程结构性修改(modCount不匹配),就立即抛出ConcurrentModificationException,防止数据异常。

      Logo

      AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

      更多推荐