前言
本文汇总全网最全Java集合知识,包含:集合体系架构、全部集合类、底层源码深度剖析、JDK1.7&1.8对比、并发集合、内存泄漏、Stream流、生产编码规约、100道面试真题标准答案。无任何知识遗漏,Java集合到此学完彻底毕业。
第一篇章 集合顶层架构(基础必懂)
1.1 集合两大分支
1.1.1 Collection(单列集合)
存储单个元素,所有单列集合的根接口,元素以单个个体形式存储,大部分集合允许元素重复,部分集合保证有序,底层包含数组、链表、堆等数据结构。
适用场景:单纯存储一组数据、批量遍历、筛选过滤、有序存储等常规数据操作。
核心子接口:List、Set、Queue。
1.1.2 Map(双列集合)
存储K-V键值对映射关系,key唯一不可重复,value可重复,一个key唯一对应一个value,无序存储(除有序子类),底层核心为哈希表、红黑树。
适用场景:键值映射查询、缓存存储、一对一关联数据、快速根据key查找value。
核心实现类:HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。
1.1.3 Collection 与 Map 核心对比(面试必背)
| 对比维度 | Collection(单列集合) | Map(双列集合) |
|---|---|---|
| 存储结构 | 单个独立元素 | Key-Value 键值对 |
| 元素唯一性 | List可重复、Set不可重复 | Key唯一,Value可重复 |
| 有序性 | List有序、Set大多无序 | HashMap无序、LinkedHashMap有序 |
| 遍历方式 | 迭代器、foreach、普通for | entrySet、keySet、values |
| 继承关系 | 继承Iterable,可迭代 | 不继承Iterable,无迭代器 |
| 常用场景 | 数组列表、队列、去重集合 | 映射关系、缓存、字典、配置映射 |
1.1.4 通用底层数据结构总览
-
数组:ArrayList、Vector,查询快、增删慢、内存连续
-
双向链表:LinkedList、LinkedHashMap,增删快、查询慢
-
哈希表:HashMap、HashSet,查询增删改查速度最优
-
红黑树:TreeMap、TreeSet,自动排序、平衡二叉树
-
最小堆:PriorityQueue,优先级排序
-
跳表:ConcurrentSkipListMap,并发有序、高性能
1.2 完整继承树
java
Iterable
├─ Collection(单列)
│ ├─ List(有序、可重复、有索引)
│ │ ├─ ArrayList
│ │ ├─ LinkedList
│ │ ├─ Vector
│ │ └─ Stack
│ ├─ Set(无序、不可重复、无索引)
│ │ ├─ HashSet
│ │ │ └─ LinkedHashSet
│ │ ├─ TreeSet
│ │ └─ EnumSet
│ └─ Queue(队列)
│ ├─ Deque(双端队列)
│ │ └─ ArrayDeque
│ ├─ PriorityQueue(优先级队列)
│ └─ BlockingQueue(阻塞队列7种)
└─ Map(双列键值对)
├─ HashMap
│ └─ LinkedHashMap
├─ TreeMap
├─ HashTable
├─ Properties
├─ WeakHashMap
├─ IdentityHashMap
├─ EnumMap
└─ ConcurrentHashMap(并发)
1.3 四大标记接口(极易遗漏)
1. Iterable(可遍历标记接口)
底层作用:顶层遍历标记,只有实现该接口的类,才能使用迭代器、foreach增强for循环遍历。
源码特征:仅包含iterator()方法,生成迭代器。
面试坑点:Collection继承Iterable,而Map不继承,所以Map不能直接foreach遍历。
2. RandomAccess(快速随机访问标记接口)
底层作用 :空接口,纯标记接口,用于标识集合是否支持高效随机访问。
实现类:ArrayList、Vector。
未实现类:LinkedList、HashSet。
生产规范 :工具类可通过instanceof RandomAccess判断集合类型,随机访问用普通for,非随机访问用迭代器,大幅提升遍历效率。
3. Serializable(序列化标记接口)
底层作用:开启Java序列化权限,空接口标记。
序列化定义:对象转为字节流,用于磁盘持久化、网络传输、RPC调用。
集合细节:所有常用集合全部实现该接口;配合transient关键字手动优化序列化,剔除冗余空元素。
踩坑点:父类实现序列化,子类自动序列化;子类实现、父类不实现,父类字段序列化丢失。
4. Cloneable(克隆标记接口)
底层作用:允许对象调用clone()方法,无该接口直接抛克隆异常。
集合特性 :Java集合全部为浅拷贝,只拷贝引用地址,不拷贝内部对象。
深浅拷贝区别:浅拷贝复制引用、修改互相影响;深拷贝新建对象、完全隔离。
生产避坑:集合存储引用类型对象,禁止直接clone,极易引发数据污染。
5. 补充:
5.1 Iterator迭代器
方法:hasNext()、next()、remove()。
特点:单向遍历、删除当前元素。
5.2 ListIterator
独有:双向遍历、添加、修改、获取索引。仅List可用。
1.3.1 四大标记接口 终极对比表(背诵版)
|--------------|----------------|----------|-------------------|
| 标记接口 | 核心作用 | 是否空接口 | 典型实现类 |
| Iterable | 支持迭代、foreach遍历 | 否(含抽象方法) | List、Set |
| RandomAccess | 高效随机下标访问 | 是 | ArrayList |
| Serializable | 对象序列化、持久化 | 是 | 全部集合 |
| Cloneable | 允许对象克隆拷贝 | 是 | ArrayList、HashMap |
1.3.2 面试高频挖坑总结(极易丢分)
-
误区1:RandomAccess有代码逻辑? 纠正:纯空标记接口,JDK通过判断接口实现,优化遍历算法。
-
误区2:所有集合都能随机访问? 纠正:LinkedList没有实现RandomAccess,下标查询效率极低。
-
误区3:Serializable必须写序列化ID? 纠正:不手动指定serialVersionUID,编译器自动生成,类修改后反序列化报错。
-
误区4:集合clone是深拷贝? 纠正:全部浅拷贝,嵌套对象依旧共享引用。
1.5 集合高阶补充(面试深挖 | 原版全网缺失)
1.5.1 transient 关键字详解(集合源码必考)
在ArrayList、HashMap等集合中,存储数组被 transient 修饰,例如:transient Object[] elementData;。
-
作用:禁止虚拟机默认序列化该数组。
-
原因:集合底层数组通常预留冗余容量(扩容空间),数组存在大量空占位元素,直接序列化会浪费IO、磁盘、网络资源。
-
序列化优化 :集合重写
writeObject(),只序列化真实有效元素,空位置不序列化。 -
结论 :transient不是完全不序列化,而是放弃JDK默认序列化,手动精准序列化。
1.4.2 集合修饰符层级规范
所有集合源码严格遵循修饰符规范,面试高频挖坑点:
-
成员变量:private + transient(私有化+禁止默认序列化)
-
静态常量:static + final(不可变、常驻方法区)
-
内部节点类:private + static(静态内部类、无外部引用、节省内存)
-
公有方法:public(对外暴露增删改查)
1.4.3 顶层接口源码极简剖析
1.4.3.1 Iterable 源码
java
public interface Iterable<T> {
// 获取迭代器
Iterator<T> iterator();
// JDK8 新增:forEach遍历
default void forEach(Consumer<? super T> action);
}
1.4.3.2 Collection 源码核心方法
java
public interface Collection<E> extends Iterable<E> {
boolean add(E e);
boolean remove(Object o);
boolean contains(Object o);
int size();
boolean isEmpty();
void clear();
}
1.4.4 集合常见异常汇总(生产避坑)
| 异常类型 | 产生原因 | 解决方案 |
|---|---|---|
| ConcurrentModificationException | 遍历中增删元素、modCount不一致 | 迭代器删除、CopyOnWriteArrayList |
| IndexOutOfBoundsException | 数组下标越界、subList截取超限 | 判断size再取值 |
| NullPointerException | TreeMap/TreeSet存null、空集合调用方法 | 非空判断 |
| ClassCastException | 泛型擦除、类型强转失败 | 严格统一泛型类型 |
1.4.5 JDK版本迭代集合重大变更(面试必问)
-
JDK1.5:引入泛型、Iterable、增强for循环,彻底告别裸集合
-
JDK1.7 :ArrayList、HashMap改为懒加载,节约初始化内存;LinkedList取消循环链表
-
JDK1.8:HashMap加入红黑树、ConcurrentHashMap放弃分段锁、新增Stream流、Lambda表达式
-
JDK1.9:of()快速创建不可变集合,禁止add/remove
-
JDK11:ArrayList新增trimToSize()缩容方法
-
JDK17:集合底层优化、垃圾回收优化、弱化synchronized锁偏向
1.4.6 泛型擦除(集合底层原理)
-
本质:泛型只在编译期生效,运行期全部擦除为Object类型
-
原理:编译器校验类型,生成字节码时去除泛型标记
-
弊端:无法使用基本类型、无法获取泛型Class、泛型数组创建失败
-
拓展:通配符 ?、? extends 上边界、? super 下边界
1.4.7 不可变集合(冷门高频面试)
1. 三种创建方式
-
JDK9+
List.of():最简单、线程安全、底层定长数组 -
Collections.unmodifiableList():包装原有集合,动态不可变
-
ImmutableList(Guava):企业开发高频使用
2. 不可变集合四大特性
-
禁止增删改,调用修改方法直接抛异常
-
线程绝对安全,无需加锁
-
内存占用极小、无冗余扩容空间
-
适用于常量配置、固定字典数据
-
只能存储引用类型,自动装箱
-
JDK5引入泛型,编译期类型校验
-
除JUC并发集合,全部非线程安全
-
底层数据结构:数组、链表、红黑树、哈希表
第二篇章 List体系(有序可重复)
2.1 ArrayList(源码深度剖析)
2.1.1 核心成员变量
java
// 真正存储元素的数组
transient Object[] elementData;
// 无参构造空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 指定0容量空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 实际元素个数
private int size;
2.1.2 构造方法(懒加载核心)
java
public ArrayList() {
// 仅仅赋值空数组,不占用堆内存
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
重点:JDK1.7之后懒加载,首次add才创建长度为10的数组,节省内存。
2.1.3 add方法源码流程
java
public boolean add(E e) {
// 校验是否需要扩容
ensureCapacityInternal(size + 1);
// 尾部赋值
elementData[size++] = e;
return true;
}
2.1.4 扩容源码(必考)
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容1.5倍:原容量 + 原容量右移一位
int newCapacity = oldCapacity + (oldCapacity >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.1.5 ArrayList核心总结
-
扩容倍数:1.5倍
-
底层拷贝:native方法 System.arraycopy
-
最大容量:Integer.MAX_VALUE - 8
-
JDK11+ 提供trimToSize()手动缩容
-
查询O(1)、中间增删O(n)
-
非线程安全,并发add会覆盖、越界、丢失数据
2.1.6 subList致命坑
-
返回原集合视图,不是新集合
-
修改子列表直接影响原列表
-
原集合增删直接抛异常
-
安全写法:new ArrayList<>(subList)
2.1.7 ArrayList 内存模型(面试深挖)
-
内存结构:连续堆内存空间,物理内存规整,CPU缓存命中率高,查询速度极快。
-
空数组内存 :DEFAULTCAPACITY_EMPTY_ELEMENTDATA 属于常量池空数组,所有无参构造对象共享,零内存占用。
-
冗余容量:扩容后预留空闲位置,减少频繁add拷贝,但会产生内存闲置。
-
GC特点:连续内存、回收干净,无内存碎片,优于链表结构。
2.1.8 并发add源码Bug详解(必考)
ArrayList非线程安全,多线程并发add会出现两大Bug:数据覆盖、数组越界异常。
-
数据覆盖原因 :
elementData[size++]非原子操作;分为取值、赋值、自增三步,多线程同时赋值会覆盖。 -
数组越界原因:多个线程同时校验扩容,判定不需要扩容,最终元素超出数组长度抛出异常。
2.1.9 JDK1.8~JDK17 ArrayList全部迭代优化
-
JDK1.8:优化数组拷贝,底层native方法内存对齐拷贝。
-
JDK11 :新增trimToSize() 手动缩容,删除冗余空闲空间。
-
JDK17:优化空数组判定、优化扩容阈值判断、减少空if判断,执行效率提升。
2.1.10 subList源码底层(致死坑原理)
subList并不是新建集合,而是返回内部静态视图类SubList,持有原集合引用。
java
private class SubList extends AbstractList<E> {
private final ArrayList<E> root;
private int offset;
}
-
修改子列表 → 直接修改原集合底层数组
-
原集合调用增删 → modCount改变,子列表立刻抛并发修改异常
-
生产铁律:永远不要直接使用subList返回对象,必须new ArrayList<>(subList)
2.1.11 ArrayList 高频刁钻面试题(压轴)
-
为什么无参构造初始为空数组,不是10? 答:懒加载设计,大量空集合节省堆内存,首次add才开辟容量为10的数组。
-
为什么扩容是右移一位(1.5倍)? 答:位运算效率极高、1.5倍属于黄金比例,平衡扩容次数与内存浪费。
-
为什么elementData要transient修饰? 答:底层数组存在大量空冗余位置,手动序列化只保存有效元素,节省IO流量。
-
最大容量为什么是Integer.MAX_VALUE - 8? 答:防止部分虚拟机对象头占用内存,避免内存溢出报错。
-
ArrayList 能否存储null? 答:可以,允许任意数量null,不做非空校验。
2.1.12 生产级优化方案(大厂规范)
-
预估数据量:初始容量 = 业务预估条数,杜绝自动扩容。
-
大批量导入:使用
addAll()批量添加,减少单次add判断。 -
数据末尾清空:使用
clear()而非新建集合,减少GC。 -
超大集合:使用分片存储,防止一次性加载导致OOM。
2.2 LinkedList(源码深度剖析)
2.2.1 底层结构
双向链表,JDK1.7取消循环链表,无数组、无扩容。
2.2.2 节点源码
java
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
2.2.3 尾部新增源码
java
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) first = newNode;
else l.next = newNode;
size++;
}
2.2.4 LinkedList总结
-
头尾增删O(1),查询遍历O(n)
-
不实现RandomAccess,禁止普通for遍历
-
可做栈、队列、双端队列
2.2.5 LinkedList 底层源码深度补充(面试深挖)
2.2.5.1 核心成员变量
java
// 实际元素个数
transient int size = 0;
// 头节点指针
transient Node<E> first;
// 尾节点指针
transient Node<E> last;
2.2.5.2 根据下标查询源码(二分优化)
LinkedList查询并非从头遍历,内部做了二分判断优化,大幅提升查找效率。
java
Node<E> node(int index) {
// 前半段:从头节点正向遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
// 后半段:从尾节点反向遍历
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
-
核心优化:判断下标处于集合前半段/后半段,减少遍历次数
-
时间复杂度:最坏依旧O(n),平均查找效率提升一倍
-
面试坑点:不要误以为LinkedList每次都从头遍历
2.2.5.3 删除源码逻辑(断链释放)
java
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 断开前驱指针
if (prev == null) first = next;
else { prev.next = next; x.prev = null; }
// 断开后继指针
if (next == null) last = prev;
else { next.prev = prev; x.next = null; }
// 置空数据,帮助GC回收
x.item = null;
size--;
return element;
}
-
手动断开指针引用,消除对象可达链,方便JVM垃圾回收
-
中间节点删除只需修改相邻节点指针,无需移位
2.2.6 LinkedList 优缺点深度总结(背诵版)
✅ 优点
-
无扩容机制:链表动态生成节点,无需提前申请连续内存,无扩容拷贝开销
-
头尾增删极致高效:头尾节点操作仅修改指针,时间复杂度O(1)
-
内存灵活:零散堆内存存储,无需连续物理内存,内存利用率高
-
天然支持双端操作:可快速实现栈、队列、双端队列数据结构
❌ 缺点
-
查询效率极低:无连续内存、无随机访问,必须遍历查找
-
内存开销大:每个节点额外存储prev、next两个指针,占用额外内存
-
CPU缓存命中率低:内存分散不连续,无法利用CPU缓存行预加载机制
-
不支持高效序列化:链表节点分散,序列化遍历开销大于数组
2.2.7 LinkedList 高频面试刁钻题
-
为什么LinkedList不实现RandomAccess? 答:底层双向链表,内存分散,无法高效随机下标访问,规避错误遍历写法,提醒开发者禁止普通for循环遍历。
-
JDK1.7为什么取消循环链表? 答:简化头尾节点操作逻辑、减少指针冗余、降低代码复杂度,避免循环指针造成内存遍历死循环。
-
LinkedList能否存储null? 答:可以,无任何非空校验,允许存储多个null元素。
-
为什么不能用普通for循环遍历? 答:每次get(index)都会触发二分遍历,循环嵌套遍历时间复杂度退化至O(n²),数据量大直接卡顿。
-
LinkedList插入一定比ArrayList快吗? 答:不一定。少量数据、尾部插入时ArrayList更快;大数据量、中间/头尾插入LinkedList更快。
2.2.8 手写极简双向链表(面试手撕)
java
public class MyLinkedList<E> {
// 节点内部类
private static class Node<E> {
E item;
Node<E> prev;
Node<E> next;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.prev = prev;
this.next = next;
}
}
// 头尾节点
private Node<E> first;
private Node<E> last;
private int size;
// 尾部添加
public boolean add(E e) {
Node<E> l = last;
Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) first = newNode;
else l.next = newNode;
size++;
return true;
}
}
2.2.9 LinkedList 生产级编码规约(避坑)
-
绝对禁止普通for循环遍历:一律使用迭代器、增强for、Stream遍历
-
业务中若需要频繁查询,优先替换为ArrayList,不要强行使用链表
-
不要频繁在链表中间插入删除,中间节点依旧需要遍历查找,效率偏低
-
大数据量下,LinkedList内存占用远高于ArrayList,优先选用数组集合
-
临时栈、队列场景,优先使用ArrayDeque,性能优于LinkedList
2.3 Vector & Stack(淘汰类)
-
Vector:数组、扩容2倍、全部方法加锁、性能极差
-
Stack:继承Vector,后进先出,官方废弃,推荐ArrayDeque
2.4 并发List
2.4.1 CopyOnWriteArrayList
-
写时复制、读写分离
-
写操作新建数组,原数组不变
-
读不加锁、写加锁
-
适用:读多写少
-
缺点:内存占用大、迭代器不能删除、数据弱一致性
2.4.2 三种线程安全List横向对比(面试必考)
|----------------------|--------------------|---------|------|----------------|
| 集合类型 | 锁机制 | 性能 | 一致性 | 适用场景 |
| Vector | 方法级synchronized全局锁 | 极差 | 强一致性 | 老旧项目,现已淘汰 |
| synchronizedList | 对象全局锁 | 较差 | 强一致性 | 读写频次均衡 |
| CopyOnWriteArrayList | 写锁+读写分离 | 读极快、写很慢 | 弱一致性 | 读多写少(白名单、配置列表) |
2.5 List高频重难点补充(生产避坑)
2.5.1 ArrayList 与 LinkedList 终极对比(背诵版)
-
底层结构:ArrayList 动态数组;LinkedList 双向链表
-
访问速度:ArrayList 随机访问O(1);LinkedList 遍历查找O(n)
-
增删效率:ArrayList 中间增删慢(数组移位);LinkedList 头尾增删O(1)
-
内存占用:ArrayList 末尾冗余空容量;LinkedList 节点存前后指针,内存开销大
-
遍历方式:ArrayList 支持普通for、增强for;LinkedList 禁止普通for,推荐迭代器/增强for
-
共同缺点:均为非线程安全,并发环境不可直接使用
2.5.2 手写极简ArrayList(面试手撕题)
java
public class MyArrayList<E> {
// 底层数组
private transient Object[] elementData;
// 实际元素个数
private int size;
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 无参构造:懒加载
public MyArrayList() {
elementData = new Object[0];
}
// 添加元素
public boolean add(E e) {
// 判断扩容
if (size == elementData.length) {
int newCapacity = elementData.length + (elementData.length >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}
elementData[size++] = e;
return true;
}
// 获取元素
public E get(int index) {
return (E) elementData[index];
}
}
2.5.3 List常见遍历删除坑+正确写法
❌ 错误写法(foreach遍历删除,必报并发修改异常)
java
for (String s : list) {
if ("test".equals(s)) {
list.remove(s);
}
}
✅ 正确写法1:迭代器删除(通用)
java
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if ("test".equals(iterator.next())) {
iterator.remove();
}
}
✅ 正确写法2:JDK8+ removeIf(生产推荐)
java
list.removeIf("test"::equals);
2.5.4 List去重五种方式(生产常用)
-
LinkedHashSet去重:保留插入顺序,最简单通用
-
Stream流去重:代码简洁,JDK8+推荐
-
循环比对去重:无需依赖其他集合,效率低
-
TreeSet去重:去重+自动排序
-
自定义规则去重:Steam distinctByKey,根据对象字段去重
2.5.5 生产编码硬性规约(List专属)
-
业务预估数据量,创建ArrayList必须指定初始化容量,规避频繁扩容拷贝
-
禁止在for循环内new ArrayList,避免频繁创建销毁对象、引发GC
-
返回集合尽量返回不可变集合,防止外部修改篡改数据源
-
大批量数据拆分批次处理,避免一次性加载导致OOM内存溢出
-
LinkedList绝对禁止使用普通for循环遍历,时间复杂度极高
第三篇章 Set体系(无序不可重复)
3.1 HashSet(源码极简)
3.1.1 底层源码
java
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3.1.2 去重原理(源码深挖)
-
先判断hashCode()
-
哈希相同再判断equals()
-
全部相同判定重复
3.1.3 HashSet 底层深度补充(面试必考)
3.1.3.1 为什么使用固定占位PRESENT?
java
private static final Object PRESENT = new Object();
-
HashSet依托HashMap存储,仅需保存key实现去重,value无业务意义
-
使用静态常量Object占位,避免频繁new对象,节约内存
-
所有HashSet元素共用同一个占位value,无内存冗余
3.1.3.2 HashSet 能否存储null?
-
允许存入一个null,不允许多个null
-
底层HashMap允许null key,哈希扰动函数判定null哈希值为0
-
重复存入null会被去重覆盖,最终仅保留一个null
3.1.3.3 自定义对象去重致命坑(生产高频Bug)
自定义实体类不重写 hashCode() & equals() ,默认继承Object原生方法:依据内存地址判断相等,导致属性相同的对象无法去重。
❌ 错误演示(去重失效)
java
// 无重写hashCode、equals
User u1 = new User("张三",18);
User u2 = new User("张三",18);
HashSet<User> set = new HashSet<>();
set.add(u1);
set.add(u2);
// 结果:两个对象全部存入,去重失败
✅ 正确写法(强制重写双方法)
java
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
3.1.3.4 HashSet 高频面试刁钻题
-
HashSet 为什么无序? 答:底层哈希表,元素按照哈希下标散列存储,不保留插入顺序。
-
HashSet 增删查时间复杂度? 答:理想O(1),哈希冲突退化后链表O(n)、树化后O(logn)。
-
HashSet 线程安全吗? 答:不安全,底层依托HashMap,并发增删会出现数据覆盖、丢失。
-
怎么实现线程安全的HashSet? 答:new CopyOnWriteArraySet<>()、Collections.synchronizedSet()。
3.2 LinkedHashSet(有序去重集合)
3.2.1 底层源码与结构
-
底层LinkedHashMap,继承HashSet、拓展有序能力
-
维护一条双向链表,记录元素插入顺序
-
哈希表存储数据、双向链表维护顺序
3.2.2 核心特性
-
有序性:保留元素插入顺序,遍历顺序等于添加顺序
-
去重性:同HashSet,依靠hashCode+equals去重
-
性能劣势:维护双向链表,内存开销略大于HashSet
-
允许null:同样仅允许存储一个null元素
3.2.3 使用场景
-
需要去重且保留插入顺序的业务场景
-
接口返回数据、有序字典、日志去重排序
-
替代HashSet,牺牲少量性能换取有序特性
3.2.4 LinkedHashSet 深度源码深挖(高频冷门面试)
3.2.4.1 无参构造源码 & 底层默认规则
java
public LinkedHashSet() {
// 底层调用LinkedHashMap,accessOrder默认false
super(16, .75f, false);
}
-
第三个布尔参数accessOrder = false:默认**插入顺序**
-
若手动改为true:开启**访问顺序(LRU顺序)**
-
全部构造方法最终指向父类HashSet的有参构造,底层强行创建LinkedHashMap
3.2.4.2 双向链表存储原理
LinkedHashSet 在哈希表基础上,额外维护一条全局双向循环链表,所有元素插入时首尾相连:
-
哈希表:负责哈希散列、快速查询、去重判断
-
双向链表:负责记录插入先后顺序,遍历严格按照链表顺序输出
-
节点额外保存 before、after 指针,相比HashSet内存多占用16字节左右
3.2.4.3 两种顺序模式(必考区别)
-
插入顺序(默认)accessOrder=false 元素顺序永远等于添加顺序,修改、查询元素不会改变顺序,业务最常用。
-
访问顺序 accessOrder=true 每调用一次get/查询元素,当前元素挪动至链表尾部,最近访问在后、最久未访问在前,天然适配LRU缓存淘汰逻辑。
3.2.4.4 LinkedHashSet 为什么有序还比TreeSet快?
-
LinkedHashSet:哈希表+链表,增删查近乎O(1),仅维护顺序指针
-
TreeSet:红黑树,每次插入需要旋转平衡、比较排序,耗时极高
-
结论:只要不需要自定义排序,优先LinkedHashSet,不要TreeSet
3.2.5 LinkedHashSet 生产致命坑(大厂踩坑记录)
-
坑1:修改元素属性,顺序不变 底层链表顺序仅在add/remove时改动,修改对象内部字段,不会重排链表。
-
坑2:去重规则严格依赖双方法 和HashSet完全一致,自定义对象不重写hashCode&equals,直接去重失效。
-
坑3:初始化容量不合理会频繁扩容 继承HashMap扩容机制,扩容重建链表、重排双向指针,开销大于HashSet。
-
坑4:遍历效率高于HashSet LinkedHashSet遍历双向链表,无需哈希散列寻址,大数据量遍历速度优于HashSet。
3.2.6 LinkedHashSet 高频刁钻面试题
-
LinkedHashSet 怎么实现顺序? 答:底层LinkedHashMap维护双向链表,记录元素插入先后位置。
-
和HashSet遍历谁更快? 答:LinkedHashSet更快,链表有序连续遍历,无需哈希寻址。
-
能否实现LRU缓存? 答:可以,反射修改accessOrder为true,开启访问排序。
-
为什么不自动扩容保持链表顺序? 答:扩容重建哈希表,双向链表顺序不会打乱,底层保留引用关系。
-
null元素存放规则? 答:同HashSet,仅允许一个null,放在链表首位。
3.2.7 生产真实业务场景
-
接口返回字典枚举,要求去重且保留录入顺序
-
日志数据分析:去重保留时间插入顺序
-
权限ID集合:不可重复、保留配置顺序
-
简单本地LRU缓存、少量数据淘汰策略
3.3 TreeSet(自动排序集合)
3.3.1 底层源码与存储结构
-
底层封装TreeMap,依托红黑树实现排序、去重
-
无哈希表、无链表,天然有序,查询时间复杂度O(logn)
3.3.2 两种排序方式(必考)
① 自然排序(实体类实现Comparable)
java
public class User implements Comparable<User>{
@Override
public int compareTo(User o) {
// 升序:this - o 降序:o - this
return this.age - o.age;
}
}
② 定制排序(构造器传入Comparator,优先级更高)
java
// 匿名比较器:年龄降序排序
TreeSet<User> set = new TreeSet<>((o1,o2)->o2.getAge()-o1.getAge());
3.3.3 TreeSet 三大硬性禁忌(生产致命坑)
-
禁止存储null:排序需要比较,null调用比较方法直接空指针异常
-
去重依据不是equals:仅依靠compareTo/compare返回0判定重复
-
存储类型必须一致:不允许存放不同类型对象,强转报错
3.3.4 TreeSet 高频面试题
-
TreeSet 为什么不能存null? 答:底层比较器需要调用比较方法,null无引用,直接抛出空指针异常。
-
TreeSet 去重规则和HashSet区别? 答:HashSet:hashCode+equals;TreeSet:compareTo返回0。
-
两种排序优先级? 答:定制排序(Comparator) > 自然排序(Comparable)。
-
TreeSet 时间复杂度? 答:增删查全部O(logn),红黑树平衡无极端退化。
3.4 Set集合横向终极对比(背诵版)
|---------------|----------|------|-----------------|--------|------------|
| Set集合 | 底层结构 | 有序性 | 去重规则 | 允许null | 适用场景 |
| HashSet | 哈希表 | 无序 | hashCode+equals | 单个null | 快速去重、无需有序 |
| LinkedHashSet | 哈希表+双向链表 | 插入有序 | hashCode+equals | 单个null | 去重+保留顺序 |
| TreeSet | 红黑树 | 自动排序 | compareTo返回0 | 禁止null | 自定义排序、有序去重 |
3.5 哈希冲突全网详解(必考深挖)
3.5.1 哈希冲突产生原因
不同key经过哈希运算,得到相同数组下标,即为哈希冲突;哈希值相同、对象不同也会产生冲突。
3.5.2 JDK两大哈希冲突解决方案
-
链地址法(拉链法):HashMap、HashSet、LinkedHashMap;冲突元素挂载链表,长度过长转为红黑树。
-
开放地址法:ThreadLocalMap;冲突后向后寻找空位,线性探测插入。
3.5.3 链地址法优缺点
-
✅ 优点:冲突处理简单、删除方便、不会出现数据堆积
-
❌ 缺点:链表过长查询变慢,需要树化优化
3.5.4 开放地址法优缺点
-
✅ 优点:内存连续、无链表节点开销、CPU缓存命中率高
-
❌ 缺点:删除困难、容易产生聚集冲突、扩容成本高
3.6 Set集合生产编码规约(避坑)
-
自定义实体存入哈希集合,必须强制重写hashCode和equals,不可省略
-
业务需要有序去重,优先使用LinkedHashSet,不要手写排序
-
TreeSet提前做非空判断,杜绝null入集合抛异常
-
高并发场景禁止使用普通HashSet,优先使用CopyOnWriteArraySet
-
预估去重数量,HashMap底层集合可提前初始化容量,减少扩容
-
先判断hashCode()
-
哈希相同再判断equals()
-
全部相同判定重复
3.3 TreeSet
-
底层TreeMap、红黑树
-
自动排序:自然排序/定制排序
-
去重依据:compareTo返回0
-
禁止存null,空指针异常
3.4 哈希冲突解决方式
-
链地址法:HashMap、HashSet
-
开放地址法:ThreadLocalMap
第四篇章 Queue队列体系(源码深度+阻塞队列+生产实战)
4.1 Queue 顶层接口架构
4.1.1 队列继承体系
java
Collection
└─ Queue(队列顶层接口)
├─ Deque(双端队列接口)
│ ├─ ArrayDeque
│ └─ LinkedList
├─ PriorityQueue(优先级队列)
└─ BlockingQueue(阻塞队列)
├─ 普通阻塞队列
└─ 双向阻塞队列BlockingDeque
4.1.2 Queue核心特性
-
核心规则:绝大多数队列遵循FIFO(先进先出),PriorityQueue为特殊优先级排序
-
存储特点:限制元素出入方向,用于流量削峰、生产消费、任务排队
-
通用禁忌:大部分队列禁止存储null元素,防止空指针判定异常
-
线程安全:普通队列非线程安全,阻塞队列天然线程安全
4.2 普通非阻塞队列(源码深挖)
4.2.1 ArrayDeque(数组双端队列|官方首选)
4.2.1.1 底层存储结构
底层可变循环数组,JDK1.6诞生,无容量限制、自动扩容,实现Deque双端接口,可同时作为队列、栈、双端队列使用。
4.2.1.2 核心成员变量
java
// 底层存储数组,容量必须为2的幂
transient Object[] elements;
// 头节点下标
transient int head;
// 尾节点下标
transient int tail;
// 默认最小容量
private static final int MIN_INITIAL_CAPACITY = 8;
4.2.1.3 扩容机制
-
底层数组容量永远是2的幂次方,位运算高效寻址
-
数组填满后自动扩容,扩容为原容量2倍
-
采用循环数组设计,头尾指针循环移动,避免数组频繁拷贝移位
4.2.1.4 核心优缺点
-
✅ 优点:无锁、性能远超LinkedList、头尾增删O(1)、内存紧凑、CPU缓存命中率高
-
❌ 缺点:不支持null元素、无同步锁、非线程安全
4.2.1.5 生产强制规约
-
官方推荐:栈场景优先ArrayDeque,废弃Stack
-
普通队列场景:优先ArrayDeque,舍弃LinkedList
-
不适合大量中间位置增删,仅适合头尾操作
4.2.2 PriorityQueue(优先级队列|最小堆)
4.2.2.1 底层数据结构
底层动态可变最小堆,基于数组实现完全二叉堆,无先进先出规则,自定义优先级排序。
4.2.2.2 核心源码常量
java
// 默认初始化容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 底层存储堆数组
transient Object[] queue;
// 比较器,自定义优先级
private final Comparator<? super E> comparator;
4.2.2.3 堆排序核心规则
-
默认自然排序:数值从小到大、字典序排序,构建最小堆
-
自定义比较器:可改为最大堆,优先级自主控制
-
仅保证堆顶(队首)为极值,其余元素无序,不会全局排序
4.2.2.4 致命坑点(面试高频)
-
禁止存储null元素,无空值容错逻辑
-
遍历输出无序,只有poll/peek能获取极值
-
底层数组扩容:容量小于64扩容2倍,大于64扩容1.5倍
-
非线程安全,并发环境使用PriorityBlockingQueue
4.2.2.5 真实业务场景
-
任务优先级调度,高优先级任务优先执行
-
排行榜TopN、海量数据筛选极值
-
延时任务简易排序、消息优先级分发
4.3 双端队列 Deque 终极对比(背诵版)
|--------|---------------|-------------|
| 对比维度 | ArrayDeque | LinkedList |
| 底层结构 | 循环数组 | 双向链表 |
| 内存占用 | 内存紧凑、无多余指针 | 节点携带双指针、开销大 |
| 增删性能 | 头尾极快,无遍历开销 | 头尾快,中间慢 |
| null存储 | 禁止存储null | 允许存储null |
| 适用场景 | 栈、普通队列、高频头尾操作 | 极少使用,仅做兼容 |
4.4 阻塞队列 BlockingQueue(JUC核心|面试必考)
4.4.1 阻塞队列核心概念
定义:支持阻塞等待的线程安全队列,内置锁机制,适配生产者-消费者模型。
-
队列满时:生产者线程阻塞,等待队列腾出空间
-
队列空时:消费者线程阻塞,等待队列存入元素
-
全部方法线程安全,无需手动加锁,生产异步解耦首选
4.4.2 队列四大API分类(完整版)
|----------|---------------|------------|-----------|-------------------|
| 操作类型 | 入队方法 | 出队方法 | 查询队首 | 异常说明 |
| 抛异常 | add() | remove() | element() | 满/空直接抛异常 |
| 返回布尔 | offer() | poll() | peek() | 返回true/false、null |
| 永久阻塞 | put() | take() | - | 线程阻塞、无限等待 |
| 超时阻塞 | offer(e,time) | poll(time) | - | 超时自动退出、返回结果 |
4.4.3 七大阻塞队列 逐类深度剖析
① ArrayBlockingQueue(有界数组阻塞队列)
-
底层结构:定长数组、有界队列,初始化必须指定容量
-
锁机制:全局唯一ReentrantLock,生产消费共用一把锁
-
唤醒机制:两个Condition(notEmpty、notFull)精准唤醒
-
排序规则:严格FIFO先进先出
-
适用场景:固定流量限流、同步生产消费、队列长度可控
② LinkedBlockingQueue(链表无界阻塞队列)
-
底层结构:单向链表,默认无界(最大容量Integer.MAX_VALUE)
-
锁机制:双锁分离(写入锁、读取锁),读写互不阻塞
-
性能优势:吞吐量远超ArrayBlockingQueue,并发能力强
-
生产坑点:无界容易无限堆积任务,引发OOM内存溢出
-
适用场景:线程池默认队列、异步任务排队、高并发缓冲
③ SynchronousQueue(无容量同步队列)
-
核心特性:无存储空间、不存储任何元素,一对一传递
-
工作模式:生产者写入后阻塞,必须等待消费者取出
-
底层实现:支持公平/非公平模式,无链表无数组
-
典型应用:Executors.newCachedThreadPool() 缓存线程池底层队列
-
适用场景:瞬时高并发、任务无堆积、即时传递执行
④ DelayQueue(延时阻塞队列)
-
底层结构:优先级最小堆+阻塞队列
-
元素要求:元素必须实现Delayed延时接口
-
出队规则:只有到期元素才能被取出,未到期永久阻塞
-
业务场景:订单超时关闭、红包过期、定时重试任务、延时监控
⑤ PriorityBlockingQueue(优先级阻塞队列)
-
底层结构:最小堆数组、无界阻塞队列
-
核心特性:线程安全的优先级队列,自动排序
-
禁忌:禁止null、必须实现比较规则
-
适用场景:高优先级任务插队、紧急任务优先执行
⑥ LinkedTransferQueue(高效转移队列)
-
升级特性:融合SynchronousQueue+LinkedBlockingQueue优点
-
预占模式:生产者可预占消费者,无需等待队列空位
-
性能排名:七大阻塞队列中吞吐量天花板
-
适用场景:超高并发生产消费、消息中间件简易实现
⑦ LinkedBlockingDeque(双向阻塞队列)
-
底层结构:双向链表、有界/无界可选
-
操作能力:头尾均可入队、出队,灵活度最高
-
锁机制:全局一把重入锁,性能偏弱
-
适用场景:双向任务调度、工作窃取队列、复杂排队逻辑
4.5 阻塞队列高频灵魂面试题
-
Array和Linked阻塞队列最大区别? 答:数组单锁、链表双锁;数组有界、链表默认无界;链表吞吐量更高。
-
SynchronousQueue为什么无容量? 答:为了实现一对一即时传递,不缓存任务,控制线程并发数量。
-
DelayQueue怎么实现延时? 答:底层最小堆排序,头部为最早到期元素,阻塞等待到期唤醒。
-
阻塞队列为什么不允许null? 答:null作为特殊返回值,用于判断队列获取元素失败,防止逻辑混淆。
-
双锁为什么比单锁性能高? 答:读写分离,生产者写队列、消费者读队列互不竞争锁资源。
4.5 队列生产编码规约+避坑指南
-
普通队列选型:一律优先ArrayDeque,废弃Stack、LinkedList做栈/队列
-
阻塞队列选型:流量可控用Array、高并发无界用Linked、瞬时并发用Synchronous
-
无界队列禁忌:生产禁止随意使用无界阻塞队列,防止任务堆积OOM
-
优先级队列禁忌:不要依赖遍历排序,仅通过poll获取极值元素
-
阻塞方法使用:业务优先使用超时阻塞offer/poll,避免线程永久阻塞卡死
-
队列空值校验:除LinkedList外,所有队列手动规避null元素
4.6 手写简易阻塞队列(面试手撕)
java
public class MyBlockingQueue<E> {
// 底层数组
private final Object[] items;
// 元素个数
private int count;
// 锁
private final ReentrantLock lock = new ReentrantLock();
// 不空、不满条件
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// 构造指定容量
public MyBlockingQueue(int capacity) {
items = new Object[capacity];
}
// 永久阻塞入队
public void put(E e) throws InterruptedException {
lock.lock();
try {
// 队列满,永久阻塞
while (count == items.length) {
notFull.await();
}
items[count++] = e;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 永久阻塞出队
public E take() throws InterruptedException {
lock.lock();
try {
// 队列空,永久阻塞
while (count == 0) {
notEmpty.await();
}
return (E) items[--count];
} finally {
lock.unlock();
}
}
}
第五篇章 Map体系(重中之重+源码深度)
5.1 HashMap源码深度剖析(面试核心)
5.1.7 HashMap节点源码(底层结构)
java
// 普通链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子树
TreeNode<K,V> right; // 右子树
TreeNode<K,V> prev; // 前驱节点
boolean red; // 红黑树颜色标记
}
-
节点修饰符解析 :key、hash被final修饰,哈希键不可修改,防止修改key导致哈希下标错乱、内存泄漏
-
树节点拓展:保留链表prev指针,树降级时无需重新遍历,优化转换性能
-
颜色标记:red=true为红节点,false为黑节点,维持红黑树平衡规则
5.1.8 JDK1.8 HashMap put方法逐行源码解析(带注释)
java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、判断哈希表是否初始化,未初始化则扩容初始化(容量16)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、计算数组下标,判断下标位置是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 3、空位置,直接新建节点插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 4、数组头节点key相同,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 5、判断是否为红黑树节点,树节点走树插入逻辑
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 6、链表节点,尾部遍历插入
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到链表尾部,新建节点追加
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度≥8,判断是否树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, n);
break;
}
// 遍历中找到相同key,跳出循环准备覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 7、存在重复key,覆盖value,返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数+1,用于fast-fail快速失败
++modCount;
// 8、元素个数超过阈值,执行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5.1.9 扩容resize()源码深度拆解(必考)
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1、原数组有容量,正常扩容2倍
if (oldCap > 0) {
// 超过最大容量,禁止扩容,阈值设为最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量、阈值全部扩容2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 2、初始化:指定容量构造方法
else if (oldThr > 0)
newCap = oldThr;
// 3、无参构造初始化,默认容量16、阈值12
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4、计算新阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE;
}
threshold = newThr;
// 5、创建新数组,开始数据迁移
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历原数组所有桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 单个节点,直接迁移计算新下标
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 红黑树节点,拆分树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 链表节点,高低位拆分
else {
// 低位链表:原下标不变
Node<K,V> loHead = null, loTail = null;
// 高位链表:原下标+旧容量
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 高位为0,保留原下标
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 高位为1,迁移至高位下标
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
hiTail = e;
}
} while ((e = next) != null);
// 低位链表存入原下标
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位链表存入新下标
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5.1.10 扩容高低位拆分原理(通俗易懂)
-
扩容后容量翻倍,二进制高位多1位二进制位
-
(e.hash & oldCap) 判断新增高位是0还是1
-
高位0:数据保留在原下标,无需移动
-
高位1:数据迁移至原下标+旧容量
-
优势:无需重新计算哈希,仅通过位运算拆分链表,迁移效率极高
5.1.11 红黑树 树化&降级 完整规则
✅ 树化触发条件(必须同时满足)
-
单链表节点长度 ≥8
-
哈希表数组容量 ≥64
❌ 不满足树化条件
数组容量<64,链表过长仅扩容、不树化,优先扩大散列空间减少冲突
✅ 树降级(链表化)触发条件
-
扩容迁移后,树节点数量**≤6**,自动降级为普通链表
-
阈值预留7、8作为缓冲,防止频繁树化降级、抖动消耗性能
红黑树五大特性(面试背诵)
-
节点只有红色、黑色两种颜色
-
根节点永久为黑色
-
所有叶子节点(空节点)为黑色
-
红色节点的子节点必须为黑色(不能连续红节点)
-
任意节点到所有叶子节点,黑色节点数量相同(黑色平衡)
5.1.12 HashMap 空值存储原理(高频坑)
-
null key存储规则:哈希扰动函数判定key==null,哈希值强制为0,固定存入数组下标0位置
-
null value存储规则:允许value为null,无任何限制
-
空值数量限制 :仅允许一个null键,重复null键会覆盖value
-
面试反问:为什么ConcurrentHashMap不允许null? 答:并发环境下,get(key)返回null无法区分:key不存在 / value本身为null,存在业务逻辑歧义,HashMap单线程无此顾虑。
5.1.13 JDK1.7 VS JDK1.8 HashMap 全方位对比表
|-------|---------------|--------------|
| 对比维度 | JDK1.7 | JDK1.8 |
| 底层结构 | 数组+单向链表 | 数组+链表+红黑树 |
| 插入方式 | 头插法(新节点插头部) | 尾插法(新节点插尾部) |
| 哈希扰动 | 4次位运算+异或 | 1次高低位异或,极简高效 |
| 扩容迁移 | 全部重新计算哈希 | 高低位拆分,无需重算 |
| 并发问题 | 扩容死循环、CPU100% | 无死循环,依旧线程不安全 |
| 初始化时机 | 创建对象直接初始化16 | 懒加载,首次put初始化 |
5.1.14 HashMap 生产高频Bug汇总(架构师避坑)
-
Bug1:未指定初始化容量,频繁扩容 问题:默认16容量,大批量数据多次扩容、数组拷贝、损耗性能 解决:初始化容量 = 预估元素个数 / 0.75 + 1
-
Bug2:自定义对象未重写hashCode+equals 问题:属性相同对象判定不同,去重失效、重复存储 解决:实体类强制重写双方法,基于业务属性计算哈希
-
Bug3:可变对象作为Key 问题:修改key属性→哈希值改变→找不到原元素,内存泄漏 解决:key必须使用不可变对象(String、Integer)
-
Bug4:并发环境使用HashMap 问题:并发put数据覆盖、丢失,JDK1.7死循环 解决:并发场景强制使用ConcurrentHashMap
-
Bug5:过度依赖null值判断 问题:无法区分key不存在和value为null,业务逻辑隐患 解决:使用containsKey()判断key是否存在
5.1.15 HashMap 手写极简源码(面试手撕必备)
java
public class MyHashMap<K,V> {
// 默认容量16、负载因子0.75
private static final int DEFAULT_CAP = 16;
private static final float LOAD_FACTOR = 0.75f;
// 哈希数组
private Node<K,V>[] table;
// 元素个数
private int size;
// 扩容阈值
private int threshold;
// 节点内部类
static class Node<K,V> {
int hash;
K key;
V value;
Node<K,V> next;
Node(int hash,K key,V value){
this.hash = hash;
this.key = key;
this.value = value;
}
}
// 无参构造
public MyHashMap() {
table = new Node[DEFAULT_CAP];
threshold = (int)(DEFAULT_CAP * LOAD_FACTOR);
}
// 哈希扰动
private int hash(Object key){
int h = key.hashCode();
return h ^ (h >>> 16);
}
// put方法
public V put(K key,V value){
// 计算哈希、下标
int hash = hash(key);
int index = hash & (table.length - 1);
Node<K,V> node = table[index];
// 下标无元素,直接插入
if(node == null){
table[index] = new Node<>(hash,key,value);
size++;
return null;
}
// 遍历链表,判断重复key
while (node != null){
if(node.hash == hash && node.key.equals(key)){
V old = node.value;
node.value = value;
return old;
}
if(node.next == null){
node.next = new Node<>(hash,key,value);
break;
}
node = node.next;
}
size++;
// 判断扩容
if(size > threshold){
resize();
}
return null;
}
// 简单扩容
private void resize(){
Node<K,V>[] oldTab = table;
int newCap = oldTab.length << 1;
Node<K,V>[] newTab = new Node[newCap];
// 遍历迁移
for (int i = 0; i < oldTab.length; i++) {
Node<K,V> e = oldTab[i];
if(e != null){
int newIndex = e.hash & (newCap - 1);
newTab[newIndex] = e;
}
}
table = newTab;
threshold = (int)(newCap * LOAD_FACTOR);
}
}
5.1.16 HashMap 灵魂高频面试题(压轴刁钻)
-
为什么哈希表容量必须是2的幂次? 答:①位运算替代取模,计算下标速度极快;②扩容高低位拆分迁移逻辑简单;③保证哈希散列均匀,减少冲突。
-
扰动函数为什么要高低位异或? 答:日常key哈希值高位变化大、低位变化小,高低位混合运算,让低位携带高位特征,减少低位哈希冲突。
-
为什么树化阈值是8,降级是6? 答:泊松分布统计,链表长度达到8概率极低;预留中间缓冲区间,防止频繁树化降级、性能抖动。
-
HashMap为什么不直接用红黑树? 答:链表节点占用内存更小、创建开销低;短链表查询速度快;红黑树维护平衡消耗资源,适合长链表。
-
负载因子0.75能否修改?生产建议? 答:可以修改;负载因子越大内存利用率高、冲突多;越小扩容频繁、浪费内存;生产禁止修改,0.75为官方最优平衡值。
-
为什么扩容后不需要重新计算哈希? 答:仅判断旧容量二进制高位,0留原位、1移高位,无需重复哈希运算,大幅优化扩容效率。
-
HashMap最大存储容量是多少? 答:数组最大容量2^30,理论存储Integer.MAX_VALUE个元素,受JVM堆内存限制。
5.2 LinkedHashMap源码(有序Map+LRU缓存核心)
5.2.1 底层结构与节点源码
底层架构 :继承HashMap,在哈希表基础上额外维护一条全局双向链表,记录节点访问/插入顺序,兼顾哈希查询性能与有序特性。
java
// LinkedHashMap专属节点,继承HashMap.Node,新增前后指针
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before;
Entry<K,V> after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 头节点、尾节点(全局双向链表)
transient Entry<K,V> head;
transient Entry<K,V> tail;
// 核心标记:true=访问顺序(LRU)、false=插入顺序(默认)
final boolean accessOrder;
-
节点拓展:相比HashMap节点,新增before、after双向指针,串联所有元素
-
双结构并存:哈希表负责快速查询,双向链表负责维护顺序
-
无额外扩容逻辑:完全复用HashMap扩容机制
5.2.2 四大构造方法源码解析
java
// 1、无参构造:默认容量16、负载因子0.75、插入顺序
public LinkedHashMap() {
super();
accessOrder = false;
}
// 2、指定初始化容量
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 3、指定容量+负载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 4、核心构造:自定义顺序模式(面试必考)
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
-
accessOrder=false(默认):插入顺序,元素顺序永远等于添加顺序,查询不改变位置
-
accessOrder=true:访问顺序,每一次get/查询元素,当前元素挪动至链表尾部,实现LRU最近最少使用
5.2.3 新增/插入源码(复用HashMap逻辑)
LinkedHashMap没有重写HashMap的put方法,仅重写**后置回调方法**,在元素插入后维护双向链表顺序。
5.2.3.1 后置回调三大空方法
java
// 访问元素后回调(accessOrder=true触发挪动节点)
@Override
void afterNodeAccess(Node<K,V> e) { }
// 插入元素后回调(维护双向链表头尾指针)
@Override
void afterNodeInsertion(boolean evict) { }
// 删除元素后回调(断开双向链表指针)
@Override
void afterNodeRemoval(Node<K,V> e) { }
设计思想:HashMap预留空模板方法,LinkedHashMap重写实现链表维护,符合模板方法设计模式,代码解耦。
5.2.4 访问顺序挪动原理(LRU核心)
当accessOrder=true时,调用get()查询元素,触发afterNodeAccess,将当前节点移动到链表尾部。
java
void afterNodeAccess(Node<K,V> e) {
Entry<K,V> last;
// 访问顺序开启 & 当前节点不是尾节点
if (accessOrder && (last = tail) != e) {
Entry<K,V> p = (Entry<K,V>)e;
// 1、断开当前节点前后指针
Entry<K,V> b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
// 2、当前节点挂载到尾部
p.before = last;
last.after = p;
tail = p;
++modCount;
}
}
-
逻辑总结:断开原位置指针 → 节点迁移至链表末尾 → 保证末尾为最近访问元素
-
遍历规则:遍历从头节点到尾节点,头部为最久未访问元素,尾部为最近访问元素
5.2.5 删除节点源码(断链释放)
java
void afterNodeRemoval(Node<K,V> e) {
Entry<K,V> p = (Entry<K,V>)e;
// 获取被删除节点的前后节点
Entry<K,V> b = p.before, a = p.after;
// 断开当前节点引用
p.before = p.after = null;
// 前驱节点后置指向后继
if (b == null)
head = a;
else
b.after = a;
// 后继节点前置指向前驱
if (a == null)
tail = b;
else
a.before = b;
}
手动断开指针引用,消除对象可达链,帮助JVM快速GC回收,避免内存滞留。
5.2.6 LRU缓存手写实现(面试手撕必考)
重写removeEldestEntry方法,自定义缓存最大容量,超出容量删除头部最久未使用元素。
java
public class LruCacheMap<K,V> extends LinkedHashMap<K,V> {
// 定义缓存最大容量
private static final int MAX_CACHE_SIZE = 8;
// 开启访问顺序、初始化容量
public LruCacheMap() {
super(16, 0.75f, true);
}
// 重写淘汰规则:返回true删除最久未使用元素
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 元素个数超过最大容量,触发淘汰
return size() > MAX_CACHE_SIZE;
}
public static void main(String[] args) {
LruCacheMap<String, Integer> cache = new LruCacheMap<>();
// 存入10个元素,仅保留最后8个,淘汰头部2个
for (int i = 1; i <= 10; i++) {
cache.put("key" + i, i);
}
// 遍历:保留最新8个元素
System.out.println(cache);
}
}
-
LRU原理:最近访问放尾部、最久未访问放头部、超出容量删除头部
-
适用场景:本地简易缓存、热点数据缓存、淘汰冷门数据
5.2.7 LinkedHashMap 优缺点深度总结
✅ 优点
-
继承HashMap,保留O(1)增删查哈希性能
-
双向链表维护顺序,支持插入/访问两种排序模式
-
天然实现LRU缓存算法,无需手动维护排序
-
遍历顺序稳定,适配有序映射业务场景
❌ 缺点
-
额外维护双向链表,内存开销大于HashMap
-
节点增删需要修改双向指针,少量损耗写入性能
-
非线程安全,并发环境需手动加锁
5.2.8 高频刁钻面试题(背诵版)
-
LinkedHashMap 为什么有序? 答:底层维护全局双向链表,所有插入/访问的节点通过before、after指针串联,遍历遵循链表顺序。
-
和HashMap遍历区别? 答:HashMap遍历无序;LinkedHashMap遍历严格遵循插入/访问顺序。
-
accessOrder两种模式业务场景? 答:false插入顺序:有序字典、接口有序返回;true访问顺序:本地LRU缓存、热点数据留存。
-
为什么不重写put方法? 答:复用HashMap哈希、扩容、树化逻辑,仅通过后置回调维护链表,简化代码、减少冗余。
-
LinkedHashMap 能否存null? 答:完全兼容HashMap规则,允许一个null key、多个null value。
5.2.9 生产编码规约(避坑)
-
常规有序业务优先使用默认插入顺序,不要开启访问顺序,避免频繁挪动节点损耗性能
-
简易本地缓存优先手写LinkedHashMap实现LRU,无需引入第三方依赖
-
高并发缓存禁止使用LinkedHashMap,推荐Caffeine、Redis高性能缓存
-
遍历LinkedHashMap不要使用普通for,直接使用迭代器/增强for,遍历效率最优
java
transient Entry<K,V> head;
transient Entry<K,V> tail;
// true:访问顺序;false:插入顺序
final boolean accessOrder;
5.2.2 LRU缓存实现
重写removeEldestEntry方法,判断是否删除最久未访问元素。
5.3 TreeMap
-
底层红黑树,key自动排序
-
key禁止为null
5.4 特殊Map(冷门必考)
-
WeakHashMap:key弱引用,GC自动回收,防内存泄漏
-
IdentityHashMap:==判断内存地址,不重写equals
-
EnumMap:枚举专用、数组存储、性能天花板
5.5 ConcurrentHashMap源码深度剖析(并发Map天花板)
5.5.1 JDK1.7 分段锁机制(淘汰旧版,面试必考对比)
5.5.1.1 底层架构
-
数据结构:Segment分段数组 + 哈希表 + 单向链表
-
锁机制:Segment继承ReentrantLock,每一个分段独立加锁
-
默认参数:Segment默认长度16、最大并发数16、初始容量2^4=16
-
加锁粒度:锁定整个分段,同一分段同一时间只能一个线程写入
5.5.1.2 优缺点总结
-
✅ 优点:分段隔离并发,相比HashTable全局锁,并发能力大幅提升
-
❌ 缺点:锁粒度依旧偏大、内存冗余、无法高效扩容、链表查询慢、最大并发固定16
5.5.2 JDK1.8 彻底重构(生产主流版本)
5.5.2.1 底层数据结构
数组 + 单向链表 + 红黑树,和HashMap结构完全一致,在此基础上增加并发安全机制。
5.5.2.2 核心锁体系(面试背诵)
-
无冲突阶段:CAS无锁写入,不阻塞、性能极高
-
哈希冲突阶段 :synchronized + 桶首节点锁,锁粒度极致细化
-
树节点阶段:TreeBin内置自旋锁,避免红黑树并发修改冲突
-
扩容阶段:多线程协助扩容,转移数据、提升扩容效率
5.5.2.3 核心成员常量(源码摘抄)
java
// 最大容量:2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始容量
static final int DEFAULT_CAPACITY = 16;
// 树化阈值:链表长度≥8转红黑树
static final int TREEIFY_THRESHOLD = 8;
// 链表降级阈值:树节点≤6转回链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 扩容辅助标记:当前桶正在扩容
static final int MOVED = -1;
// 树节点标记:当前桶为红黑树
static final int TREEBIN = -2;
// 占位节点:预留空节点
static final int RESERVED = -3;
// 并发扩容控制阈值
static final int CONCURRENT_TRANSFER_BITS = 16;
5.5.3 JDK1.8 put() 完整源码流程(逐行剖析)
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、判断数组是否初始化,未初始化则CAS初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、计算下标,当前桶无元素,CAS直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = new Node(hash, key, value, null);
// 3、当前桶存在元素,产生哈希冲突
else {
Node<K,V> e; K k;
// 4、桶首节点key重复,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 5、判断是否为红黑树节点,走树节点插入逻辑
else if (p instanceof TreeBinNode)
e = ((TreeBinNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 6、普通链表遍历插入
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到链表尾部,尾插法新增节点
if ((e = p.next) == null) {
p.next = new Node(hash, key, value, null);
// 链表长度≥8,触发树化判断
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 遍历过程中找到重复key,覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 存在重复key,覆盖旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数+1,用于快速失败
++modCount;
// 元素个数累加,判断是否触发扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5.5.4 CAS + synchronized 加锁底层详解
5.5.4.1 CAS无锁写入
-
适用场景:桶下标位置为空,无任何元素
-
底层原理:Unsafe类本地方法,内存地址偏移量修改,无锁、无阻塞、自旋重试
-
优势:无上下文切换、CPU开销极低,高并发空桶写入性能炸裂
5.5.4.2 synchronized 桶首节点锁
-
锁定对象:当前哈希桶的第一个节点,而非对象、而非类
-
锁粒度:最小粒度,不同桶之间互不阻塞、并发互不影响
-
锁优化:JDK1.6后偏向锁、轻量级锁、重量级锁逐级膨胀,无竞争几乎无开销
-
淘汰ReentrantLock原因:synchronized底层虚拟机优化、内存占用更小、调度更快
5.5.5 多线程协助扩容(核心亮点)
5.5.5.1 扩容触发条件
-
元素数量 > 扩容阈值(capacity * 0.75)
-
链表树化、数组容量不足被动触发
5.5.5.2 扩容机制原理
-
扩容倍数:固定2倍,和HashMap一致
-
扩容标识 :原数组桶节点hash值改为MOVED(-1),标记当前桶正在迁移
-
协助扩容:新线程写入时发现桶正在扩容,主动帮忙迁移数据,而非阻塞等待
-
迁移规则:高低位拆分,无需重算哈希,仅迁移至原下标 / 原下标+旧容量
5.5.5.3 扩容优势
单线程扩容耗时严重,多线程分摊迁移压力,高并发场景扩容耗时大幅降低,解决JDK1.7扩容卡顿痛点。
5.5.6 红黑树 TreeBin 自旋锁
-
红黑树节点复杂,不适合使用重量级锁,采用自旋锁+状态位控制并发
-
TreeBin内部维护lockState状态位:无锁、加锁、等待、标记
-
线程循环自旋尝试获取锁,不阻塞线程、不挂起,适合短时间树节点修改
-
避免多线程同时修改红黑树结构,防止树结构紊乱、死循环
5.5.7 为什么不能存null键、null值(必考)
原文简单说明,此处深度补全底层逻辑:
-
业务歧义:并发环境下,调用get(key)返回null,无法区分:key不存在 / value本身为null
-
单线程HashMap:无并发竞争,可通过containsKey()判断,所以允许null
-
并发兜底:ConcurrentHashMapput方法内部做非空校验,key/value为null直接抛出空指针异常
-
源码佐证:hash方法判断key==null直接抛NPE,不会赋值hash=0
5.5.8 JDK1.7 VS JDK1.8 ConcurrentHashMap 终极对比表
|-------|------------------|----------------------|
| 对比维度 | JDK1.7 | JDK1.8 |
| 底层结构 | Segment+数组+单向链表 | 数组+链表+红黑树 |
| 锁实现方式 | ReentrantLock分段锁 | CAS+synchronized桶节点锁 |
| 锁粒度 | 分段锁定(粒度粗) | 桶首节点锁定(粒度极细) |
| 最大并发数 | 固定16 | 无固定上限,取决于桶数量 |
| 扩容方式 | 单线程扩容,效率低 | 多线程协助扩容 |
| 树化机制 | 无红黑树,链表无限拉长 | 链表≥8树化,优化查询 |
| 空值存储 | 禁止null键、null值 | 禁止null键、null值 |
5.5.9 生产高频Bug & 避坑规约
-
Bug1:滥用containsKey判断空值 问题:不允许null值,不会出现value为空的业务歧义,无需冗余判断; 解决:直接get获取,空值直接判定不存在。
-
Bug2:大批量数据未初始化容量 问题:默认16容量,频繁扩容、多线程迁移损耗性能; 解决:初始化容量 = 预估数量 / 0.75,向上取整且保证2的幂。
-
Bug3:迭代过程修改元素 问题:依旧遵循fail-fast快速失败,遍历期间修改modCount抛异常; 解决:使用compute、merge原子方法修改。
-
Bug4:复合业务未加锁 问题:单个put/get原子安全,复合逻辑(查询+修改)非原子; 解决:业务复合操作外层手动加锁,或使用lambda原子方法。
5.5.10 高频刁钻面试题(压轴背诵)
-
为什么JDK1.8放弃ReentrantLock改用synchronized? 答:①synchronized经过虚拟机深度优化,性能反超;②锁粒度细化到桶节点,竞争极低;③底层无额外对象开销,内存更省。
-
并发扩容会不会数据丢失? 答:不会,迁移前标记MOVED状态,线程互斥迁移,高低位拆分无覆盖。
-
ConcurrentHashMap 支持排序吗? 答:不支持,底层无序;并发有序使用ConcurrentSkipListMap(跳表)。
-
为什么红黑树要用自旋锁? 答:树节点修改耗时短、频率低,自旋无阻塞,比重量级锁开销更小。
-
高并发大量哈希冲突怎么优化? 答:重写hashCode优化散列、增大初始化容量、避免自定义复杂对象key。
5.5.11 常用原子方法(生产高频)
ConcurrentHashMap提供大量原子复合方法,规避手动加锁:
java
// 存在则不覆盖
V putIfAbsent(K key, V value);
// 原子计算更新
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);
// 合并数据,存在则合并、不存在则新增
V merge(K key, V value, BiFunction<V, V, V> remappingFunction);
// 批量查询、遍历
void forEach(BiConsumer<? super K, ? super V> action);
- 生产建议:多线程统计、计数、累加场景,优先使用merge/compute,无锁安全、代码简洁。
第六篇章 迭代器与并发修改
6.1 fail-fast 快速失败
-
非并发集合:ArrayList、HashMap
-
原理:modCount != expectedModCount
-
遍历期间增删直接抛异常
6.2 fail-safe 安全失败
-
CopyOnWriteArrayList
-
遍历读取快照,原数组不修改
6.3 foreach删除报错原因
foreach底层迭代器,调用集合remove()导致修改次数不一致。
第七篇章 JUC并发集合大全(生产高频+源码深挖)
7.1 JUC并发集合总览
JUC(java.util.concurrent)包下专为高并发场景设计的线程安全集合,摒弃老旧全局同步锁,采用CAS、细粒度锁、读写分离、无锁算法,并发性能远超Vector、HashTable,是生产环境并发编程首选。
JUC并发集合四大分类:
-
并发List:CopyOnWriteArrayList、CopyOnWriteArraySet
-
并发Map:ConcurrentHashMap、ConcurrentSkipListMap
-
并发Set:ConcurrentSkipListSet、CopyOnWriteArraySet
-
阻塞队列(核心重点):七大阻塞队列,线程池底层依赖
7.2 读写分离集合:CopyOnWrite系列
7.2.1 CopyOnWriteArrayList(并发List天花板)
7.2.1.1 底层核心原理
-
底层结构:可变数组,volatile修饰数组引用
-
核心思想:写时复制、读写分离
-
锁机制:写操作加ReentrantLock独占锁,读操作无锁
-
执行流程:新增/修改/删除时,复制原数组生成新数组,修改完成后原子替换数组引用,原数组供读线程访问
7.2.1.2 核心源码摘抄
java
// 底层数组,volatile保证引用可见性
private transient volatile Object[] array;
// 写入加锁,避免多线程并发复制
final ReentrantLock lock = new ReentrantLock();
public boolean add(E e) {
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制新数组,长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 原子替换数组引用
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
7.2.1.3 优缺点&生产场景
-
✅ 优点:读无锁、查询吞吐量极高、遍历不抛并发修改异常、线程绝对安全
-
❌ 缺点:写入频繁拷贝数组、内存占用大、写入性能差;数据存在弱一致性(遍历读取旧快照)
-
生产场景 :严格读多写少,如白名单、配置列表、本地静态字典、定时推送数据
7.2.1.4 致命坑(生产必避)
-
大批量写入频繁触发数组拷贝,产生大量临时数组,引发频繁GC
-
迭代器仅可读,不支持add/remove,直接抛出异常
-
遍历数据为旧快照,无法实时获取最新数据
7.2.2 CopyOnWriteArraySet
-
底层依托:CopyOnWriteArrayList
-
去重原理:新增前调用contains()判断元素是否存在,存在则放弃新增
-
特性:去重+线程安全+读多写少,允许存储null元素
-
劣势:去重需要遍历数组,大数据量查询效率极低
-
适用场景:少量元素、去重、极少修改的静态集合
7.3 并发有序集合:跳表实现
7.3.1 ConcurrentSkipListMap
7.3.1.1 底层原理
-
底层数据结构:跳表(SkipList),多层有序链表,替代红黑树
-
排序规则:key自然排序,自定义Comparator定制排序
-
锁机制:无锁设计,CAS+自旋实现并发安全
-
对比TreeMap:TreeMap单线程红黑树;ConcurrentSkipListMap并发有序、查询O(logn)
7.3.1.2 核心特性
-
线程安全、天然有序、支持高并发读写
-
key禁止为null,value允许null
-
内存占用低于红黑树,插入删除无需平衡旋转
7.3.1.3 面试高频题
-
为什么并发有序不用TreeMap? 答:TreeMap非线程安全,加锁后性能极差;跳表无锁并发,有序且吞吐量更高。
-
跳表对比红黑树优势? 答:结构简单、无旋转平衡开销、CAS无锁并发、遍历效率更高。
7.3.2 ConcurrentSkipListSet
-
底层依托ConcurrentSkipListMap
-
无序去重+自动排序+并发安全
-
适用:并发环境下需要排序去重的集合场景
7.4 七大阻塞队列(线程池核心|面试必考)
7.4.1 阻塞队列通用核心特性
-
核心作用:缓存元素、解耦生产者与消费者、平衡上下游处理速度
-
阻塞机制:队列满时阻塞写入、队列空时阻塞读取
-
四大API规范(背诵版)
|----------|-------------|------------|-----------------|
| 方法类型 | 新增方法 | 获取/删除 | 异常表现 |
| 抛异常 | add() | remove() | 满/空直接报错 |
| 返回布尔 | offer() | poll() | 满/空返回false/null |
| 阻塞等待 | put() | take() | 无限阻塞线程 |
| 超时等待 | offer(time) | poll(time) | 超时直接退出 |
7.4.2 七大阻塞队列逐类剖析
7.4.2.1 ArrayBlockingQueue(数组阻塞队列)
-
底层结构:定长数组、有界队列、先进先出
-
锁机制:全局唯一ReentrantLock,读写共用一把锁
-
特性:容量固定不可扩容、不允许null元素、公平/非公平锁可配置
-
生产场景:固定吞吐量的生产者消费者、普通线程池任务队列
7.4.2.2 LinkedBlockingQueue(链表阻塞队列)
-
底层结构:单向链表、无界队列(默认容量Integer.MAX_VALUE)
-
锁机制:两把ReentrantLock(写入锁、读取锁),读写互不阻塞
-
特性:吞吐量高于数组队列、无边界容易OOM、不存null
-
生产场景:Executors默认线程池队列、高吞吐异步任务
7.4.2.3 SynchronousQueue(同步队列)
-
核心特性 :无容量、零存储,一对一数据传递
-
原理:生产者写入必须等待消费者读取,反之亦然
-
模式:公平模式(队列)、非公平模式(栈)
-
生产场景:紧急任务、立即执行线程、Executors缓存线程池
7.4.2.4 DelayQueue(延时阻塞队列)
-
底层结构:最小堆、无界阻塞队列
-
特性:元素必须实现Delayed接口,到期才能被取出
-
排序规则:剩余延时时间从小到大排序
-
生产场景:订单超时关闭、短信延时推送、定时过期缓存
7.4.2.5 PriorityBlockingQueue(优先级阻塞队列)
-
底层结构:最小堆、无界队列、自动扩容
-
特性:元素自动优先级排序、非先进先出、允许自定义比较器
-
坑点:同优先级元素顺序无序、不允许null
-
生产场景:消息优先级处理、紧急任务插队
7.4.2.6 LinkedTransferQueue(转移队列)
-
升级版无界队列:融合SynchronousQueue+LinkedBlockingQueue特性
-
核心方法:transfer(),无消费者时阻塞,有消费者直接传递
-
优势:并发性能最优、吞吐量最高、JDK7新增
-
场景:超高并发异步消息、中间件底层队列
7.4.2.7 LinkedBlockingDeque(双向阻塞队列)
-
底层结构:双向链表、有界/无界可选
-
特性:头尾均可增删,兼具队列+栈特性
-
优势:双向操作灵活、适合工作窃取线程池
-
场景:定时任务、双向数据存取、批量头尾处理
7.4.3 七大阻塞队列终极对比表(背诵版)
|-----------------------|-----|------|-----------|------------|
| 队列名称 | 边界性 | 底层结构 | 核心特点 | 适用场景 |
| ArrayBlockingQueue | 有界 | 数组 | 单锁、固定容量 | 普通线程池、限流任务 |
| LinkedBlockingQueue | 无界 | 单向链表 | 双锁、高吞吐 | 大批量异步任务 |
| SynchronousQueue | 无容量 | 栈/队列 | 一对一传递、无缓存 | 即时执行、短任务 |
| DelayQueue | 无界 | 最小堆 | 延时到期读取 | 超时任务、延时缓存 |
| PriorityBlockingQueue | 无界 | 最小堆 | 优先级自动排序 | 优先级消息处理 |
| LinkedTransferQueue | 无界 | 链表 | 高性能、无锁优化 | 超高并发中间件 |
| LinkedBlockingDeque | 有界 | 双向链表 | 头尾双向操作 | 工作窃取线程池 |
7.5 JUC并发集合高频面试刁钻题
-
CopyOnWriteArrayList 为什么弱一致性? 答:遍历读取旧数组快照,写入生成新数组,遍历期间无法感知最新数据,适合不要求实时性的读多写少场景。
-
阻塞队列为什么禁止存储null? 答:null作为特殊标记,用于判断队列读取终止,存储null会混淆空数据与终止标记。
-
Array和Linked阻塞队列锁区别? 答:Array单锁读写互斥;Linked双锁读写分离,互不阻塞、吞吐量更高。
-
SynchronousQueue为什么无容量? 答:设计初衷为线程直连传递,不缓存任务,避免任务堆积,适合高频短任务。
-
并发有序集合怎么选型? 答:少量有序用Collections.synchronizedSortedMap;高并发有序用ConcurrentSkipListMap。
7.6 生产编码规约(JUC专属)
-
无界阻塞队列谨慎使用,大数据量极易引发OOM内存溢出,必须手动限制容量
-
读多写少业务优先CopyOnWriteArrayList,杜绝synchronizedList低效全局锁
-
延时任务优先DelayQueue,无需额外定时任务框架,轻量高效
-
线程池队列禁止使用无界LinkedBlockingQueue,防止任务无限堆积
-
超高并发键值存储,一律使用ConcurrentHashMap,禁止任何同步包装集合
第八篇章 工具类全部盲点
8.1 Collections
-
EMPTY_LIST:全局空常量、不可修改
-
singletonList:单元素不可变集合
-
synchronizedList:全局锁、性能差
-
reverse、shuffle、swap、max、min
8.2 Arrays.asList() 五大坑
-
返回内部类,不是ArrayList
-
不能add/remove
-
基本类型数组被当成一个元素
-
修改元素影响原数组
-
无扩容方法
第九篇章 内存泄漏(架构师必懂)
-
static静态集合:常驻内存永不回收
-
ThreadLocalMap:key弱引用、value强引用,不remove泄漏
-
自定义可变对象作为HashMap key,属性修改内存泄漏
-
长生命周期集合持有短生命周期对象
-
双括号初始化:匿名内部类持有外部引用
第十篇章 Stream流式编程完整版(源码深挖+生产实战+避坑指南)
10.1 Stream核心概述(底层定位)
Stream是JDK8重磅新增 的集合处理API,基于Lambda表达式,采用流水线思想处理集合、数组数据,摒弃传统循环遍历的冗余代码。Stream不存储数据、不修改原数据源、延迟执行,仅对数据做计算、转换、筛选、归并操作,是生产环境代码极简优化的核心工具。
10.1.1 四大核心特性(面试必背)
-
无存储:Stream不是数据容器,不保存元素,仅对流式传输的数据做处理,数据源为集合、数组、IO流。
-
不可变:所有操作不会修改原数据源,处理后生成新流/新集合,无副作用。
-
延迟执行:中间操作仅封装执行逻辑,无实际运算;触发终止操作才会执行全部流水线。
-
链式编程:方法链式调用,代码简洁紧凑,可读性极强。
10.1.2 Stream核心组成架构
完整流水线分为三步,缺一不可:
-
创建流(源头):获取数据源对应的Stream流
-
中间操作(加工):筛选、映射、排序、去重等,返回新流,可链式叠加
-
终止操作(收尾):触发执行、收集结果、遍历统计,执行后流关闭不可复用
10.2 流的创建方式(生产常用)
10.2.1 常用创建源码示例
java
// 1、集合创建(最常用)
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
// 2、数组创建
String[] arr = {"a","b","c"};
Stream<String> arrStream = Arrays.stream(arr);
// 3、直接创建空流
Stream<Object> emptyStream = Stream.empty();
// 4、创建无限流(生产慎用,防止OOM)
Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);
// 5、基本类型专用流(自动装箱、节省内存)
IntStream intStream = IntStream.of(1,2,3);
10.2.2 流分类
-
串行流:stream(),单线程执行,执行顺序可控、无线程安全问题
-
并行流:parallelStream(),多线程拆分执行,大数据量提速,无序、非线程安全
-
基本类型流:IntStream、LongStream、DoubleStream,规避自动装箱拆箱损耗
10.3 中间操作(全部汇总+生产案例)
中间操作分为:无状态操作 (不依赖上一个元素)、有状态操作(需要缓存全部元素,如排序、去重)。所有中间操作延迟执行,不触发终止操作不运行。
10.3.1 无状态中间操作
|---------|------------------|---------------|
| 方法名 | 作用 | 生产案例 |
| filter | 条件过滤,保留符合条件元素 | 筛选年龄大于18的用户 |
| map | 元素映射、类型转换 | 用户集合提取用户名 |
| flatMap | 扁平化拆解,拆分嵌套集合 | 嵌套列表拆解为单层列表 |
| peek | 遍历操作、调试打印、修改元素属性 | 批量修改用户状态、打印日志 |
| skip | 跳过指定个数元素 | 分页查询跳过前N条数据 |
| limit | 截取指定个数元素 | 只获取前10条有效数据 |
10.3.2 有状态中间操作
|--------------------|--------------------------|---------------------|
| 方法名 | 作用 | 注意事项 |
| distinct | 元素去重,底层重写equals+hashCode | 自定义对象必须重写双方法,否则去重失效 |
| sorted() | 自然排序,元素实现Comparable接口 | 字符串、数字默认排序,自定义对象报错 |
| sorted(Comparator) | 自定义比较器排序,优先级更高 | 支持多字段组合排序、升降序切换 |
10.3.3 高频中间操作代码实战(生产直接复用)
java
// 模拟用户集合
List<User> userList = new ArrayList<>();
// 筛选+映射+排序+截取 链式操作
List<String> userNameList = userList.stream()
// 筛选成年用户
.filter(user -> user.getAge() >= 18)
// 根据年龄倒序排序
.sorted(Comparator.comparing(User::getAge).reversed())
// 跳过前2条
.skip(2)
// 截取10条
.limit(10)
// 提取用户名
.map(User::getName)
// 收集为集合
.collect(Collectors.toList());
10.4 终止操作(核心重点+收集器详解)
终止操作是流水线的开关,执行后流销毁,不可二次使用;分为:收集类、遍历类、统计类、匹配类、归并类。
10.4.1 常用终止操作分类
-
收集collect():最核心,流转集合、数组、Map、分组、拼接
-
遍历forEach():循环执行逻辑,无返回值
-
统计计算:max、min、count、sum、average
-
逻辑匹配:anyMatch、allMatch、noneMatch,返回布尔值
-
归并reduce:迭代累加、聚合计算,实现自定义聚合逻辑
-
获取元素:findFirst、findAny,获取流中单个元素
10.4.2 Collectors收集器大全(生产天花板)
Collectors是Stream生产最高频工具类,封装全部收集逻辑,无需手动转换。
java
// 1、常规收集
Collectors.toList(); // ArrayList
Collectors.toSet(); // HashSet
Collectors.toMap(key,value); // 转为Map
// 2、分组收集(业务高频)
Collectors.groupingBy(User::getAge); // 单字段分组
Collectors.groupingBy(User::getSex, Collectors.counting()); // 分组统计数量
// 3、拼接字符串
Collectors.joining(","); // 元素逗号拼接
// 4、去重+有序
Collectors.toCollection(LinkedHashSet::new);
// 5、分区(布尔分组,分为true/false两组)
Collectors.partitioningBy(user -> user.getAge() > 18);
10.4.3 reduce归并原理(面试深挖)
reduce将流中元素反复结合,实现累加、求积、最大值等聚合计算,底层迭代遍历,无额外循环。
java
// 数字累加
List<Integer> numList = Arrays.asList(1,2,3,4);
Integer sum = numList.stream().reduce(0, Integer::sum);
// 最大值
Integer max = numList.stream().reduce(Integer::max).get();
10.5 并行流(底层原理+适用场景+避坑)
10.5.1 并行流底层原理
-
底层依赖:ForkJoinPool分支合并线程池、Spliterator拆分迭代器
-
执行流程:将大数据量集合拆分多个子任务,多线程并行执行,最后合并结果
-
默认线程池:使用公共ForkJoinPool,线程数等于CPU核心数
10.5.2 并行流优缺点
✅ 优点
-
大数据量下执行速度远快于串行流
-
无需手动创建线程,极简实现多线程运算
❌ 缺点
-
执行顺序随机、无序,无法保证遍历顺序
-
非线程安全,禁止操作共享变量
-
拆分、合并存在开销,小数据量性能反而更差
10.5.3 并行流生产禁忌(必背)
-
禁止使用并行流操作有状态、共享变量,会出现数据错乱
-
IO密集型任务不推荐并行流,适合CPU密集型纯计算任务
-
数据量小于1万,一律使用串行流,避免拆分开销
10.6 Stream高频致命坑(生产Bug汇总)
-
流只能使用一次 错误:重复调用终止操作;纠正:流水线一次性执行,禁止复用流对象。
-
toMap键重复报错 问题:key重复抛IllegalStateException;纠正:第四个参数设置合并规则 (v1,v2)->v1。
-
并行流修改共享变量 问题:多线程并发修改,数据覆盖错乱;纠正:并行流保持无状态,不修改外部变量。
-
distinct去重自定义对象失效 原因:未重写equals+hashCode;纠正:哈希集合、Stream去重必须重写双方法。
-
peek修改不可变对象无效 String、Integer不可变类型,peek修改不会生效,仅支持自定义引用类型修改属性。
-
空集合直接get()报错 findFirst().get() 无元素抛空指针;纠正:使用orElse、orElseGet设置默认值。
10.7 Stream生产级编码规约(大厂规范)
-
优先使用链式写法,代码扁平化,禁止拆分零散定义流对象。
-
筛选操作前置:filter、skip、limit尽量靠前,减少后续运算数据量。
-
排序、去重后置:有状态操作放在无状态操作之后,减少缓存开销。
-
Map转换优先指定泛型,避免toMap泛型擦除,防止类型转换异常。
-
空元素防护:使用Optional包装元素,杜绝空指针异常。
-
大数据量分批处理,并行流仅用于CPU密集型、无状态计算任务。
-
禁止在Stream中写复杂业务逻辑,Lambda保持极简,提高可读性。
10.8 Stream高频面试刁钻题(压轴)
-
Stream为什么延迟执行? 答:中间操作封装逻辑不运算,终止操作一次性执行,减少循环次数、提升执行效率,合并多次遍历为一次。
-
Stream和for循环哪个快? 答:少量数据for循环更快;中等数据串行流持平;大数据量并行流碾压for循环。
-
并行流为什么不推荐业务使用? 答:顺序不可控、线程不安全、公共线程池容易被占用,影响全局线程调度。
-
peek和map区别? 答:peek无返回值,修改原有对象属性;map有返回值,做类型转换、生成新元素。
-
为什么流不能重复使用? 答:流内部标记执行状态,终止操作执行后标记关闭,防止重复运算、数据错乱。
第十一篇 集合扩容机制全网汇总(零遗漏完整版)
扩容是集合核心底层机制,所有可变集合初始化时不会开辟大量内存,在元素达到阈值后自动扩容,本篇汇总Java全部常用集合扩容规则、触发条件、源码底层、拷贝方式、性能优劣、面试坑点,全网最全扩容合集。
11.1 核心基础概念(前置必懂)
-
初始容量:集合初始化默认开辟的底层数组长度
-
阈值(threshold):触发扩容的临界值 = 容量 * 负载因子
-
负载因子(loadFactor):控制集合填充密度,平衡哈希冲突与内存占用
-
扩容本质:新建更大容量数组 + 原数组数据拷贝 + 原数组GC回收
-
拷贝方式 :绝大多数集合底层使用native方法
System.arraycopy(),内存级拷贝效率极高
11.2 单列集合扩容详解
11.2.1 ArrayList(重中之重)
-
底层结构:动态Object数组、JDK1.7+懒加载
-
初始容量:无参构造默认空数组,首次add扩容为10;有参构造为指定容量
-
扩容倍数 :1.5倍(原容量 + 原容量右移一位:oldCapacity + (oldCapacity >> 1))
-
扩容触发条件:新增元素后实际元素个数 > 底层数组长度
-
最大容量:Integer.MAX_VALUE - 8(避免虚拟机内存对齐溢出)
-
特殊特性:JDK11新增trimToSize()手动缩容,释放冗余空闲空间
-
缺点:扩容需数组拷贝,大数据量频繁扩容损耗性能
11.2.2 Vector(淘汰类)
-
初始容量:默认10,无懒加载,初始化直接创建数组
-
扩容倍数 :2倍(固定翻倍,无位运算优化)
-
扩容触发:元素个数超出数组长度
-
附加特性:所有方法加synchronized全局锁,扩容性能极差,生产彻底淘汰
11.2.3 LinkedList(无扩容)
-
扩容机制 :无扩容概念
-
底层原理:双向链表,动态创建节点,零散分配堆内存,无需提前预留空间
-
优缺点:无拷贝开销、无内存冗余;但每个节点额外存储指针,内存开销大
11.2.4 ArrayDeque(双端队列)
-
初始容量:默认16,必须为2的幂次方
-
扩容倍数 :2倍扩容
-
扩容触发:数组填满,判断头尾指针重合
-
底层优化:环形数组,通过位运算快速定位下标,无取模运算开销
11.2.5 PriorityQueue(优先级队列)
-
初始容量:默认11
-
扩容规则:容量小于64时扩容2倍;大于64扩容1.5倍
-
底层结构:最小堆数组,扩容后重新调整堆结构,维持排序特性
11.3 双列集合扩容详解(面试高频)
11.3.1 HashMap(核心必考)
-
初始容量:默认16,必须为2的幂次方;自定义容量会自动向上取最近2的幂
-
负载因子:默认0.75(泊松分布优化,平衡冲突与空间)
-
扩容阈值:容量 * 0.75,达到阈值触发扩容
-
扩容倍数 :固定2倍扩容,位运算快速计算新容量
-
扩容触发时机:元素个数达到阈值 | 链表长度≥8且数组长度<64(先扩容后树化)
-
JDK1.8扩容优化:原数组元素拆分迁移,无需重新计算哈希,通过高位判定分到新旧下标
-
最大容量:1 << 30(2的30次方)
11.3.2 LinkedHashMap
-
扩容规则:完全继承HashMap,初始容量16、负载因子0.75、2倍扩容
-
额外特性:扩容后保留双向链表顺序,不打乱插入/访问顺序
11.3.3 HashTable(淘汰类)
-
初始容量:默认11(非2的幂)
-
负载因子:默认0.75
-
扩容倍数 :2倍+1(newCapacity = oldCapacity * 2 + 1)
-
劣势:全局synchronized锁、扩容算法冗余、性能极差
11.3.4 TreeMap(无常规扩容)
-
扩容机制 :无数组扩容概念
-
底层原理:红黑树,动态新增树节点,无需连续数组内存,无拷贝开销
-
平衡机制:节点过多通过变色、旋转维持树平衡,替代扩容逻辑
11.4 并发集合扩容规则(JUC专属)
11.4.1 ConcurrentHashMap
-
初始容量:默认16,强制2的幂次方
-
负载因子:默认0.75
-
扩容倍数:2倍扩容
-
特殊优化:支持并发扩容,多线程协助迁移数据,大幅提升扩容效率
-
扩容阈值:size > 容量*0.75,且检测到扩容标记位则触发协助扩容
11.4.2 CopyOnWriteArrayList
-
扩容规则 :每次写入原长度+1,无固定倍数
-
底层原理:写时复制,新增元素直接新建长度+1的数组,拷贝原数据
-
劣势:大批量写入频繁创建数组,内存占用极高,仅适合读多写少
11.5 负载因子深度剖析(面试必考)
-
负载因子定义:衡量集合数组填充密集程度,公式:负载因子=元素个数/数组容量
-
默认0.75原因 :依据泊松分布,哈希冲突概率最低,完美平衡内存占用 与查询效率
-
负载因子偏大(如1.0):数组填满才扩容,节省内存;哈希冲突激增,链表变长,查询效率暴跌
-
负载因子偏小(如0.5):提前扩容,冲突极少;内存大量闲置,浪费堆空间
11.6 全部集合扩容规则汇总表(背诵版)
|----------------------|---------|--------|------|---------------|
| 集合名称 | 初始容量 | 扩容倍数 | 负载因子 | 核心特点 |
| ArrayList | 懒加载(10) | 1.5倍 | 无 | 数组拷贝、支持缩容 |
| Vector | 10 | 2倍 | 无 | 全局锁、性能极差 |
| HashMap | 16 | 2倍 | 0.75 | 2的幂、高位迁移 |
| HashTable | 11 | 2倍+1 | 0.75 | 非2的幂、已淘汰 |
| ArrayDeque | 16 | 2倍 | 无 | 环形数组、双端操作 |
| PriorityQueue | 11 | 1.5/2倍 | 无 | 小容量2倍、大容量1.5倍 |
| ConcurrentHashMap | 16 | 2倍 | 0.75 | 并发扩容、多线程协助 |
| CopyOnWriteArrayList | 0 | +1 | 无 | 写时复制、单次扩容1位 |
| LinkedList/TreeMap | - | 无 | 无 | 链表/红黑树、无扩容 |
11.7 扩容高频面试坑点(极易丢分)
-
**坑1:ArrayList初始化容量为10?**纠正:JDK1.7+无参构造是空白数组,首次add才初始化容量10,懒加载设计。
-
**坑2:HashMap自定义容量随意写?**纠正:必须传2的幂,否则底层自动向上换算,浪费计算性能。
-
**坑3:HashMap链表达到8直接树化?**纠正:必须同时满足链表长度≥8 数组容量≥64,否则优先扩容。
-
**坑4:扩容一定会拷贝全部元素?**纠正:JDK1.8的HashMap扩容,无需重新哈希,仅拆分高低位,优化拷贝效率。
-
**坑5:负载因子越小越好?**纠正:过小频繁扩容、浪费内存;过大冲突严重、查询变慢,0.75为最优官方定值。
-
**坑6:CopyOnWriteArrayList适合大批量写入?**纠正:每次扩容+1,频繁新建数组,大批量写入严重卡顿、OOM。
11.8 生产扩容优化规约(大厂规范)
-
预估数据初始化容量 :HashMap初始化容量计算公式:预估元素个数 / 0.75 + 1,规避自动扩容。
-
禁止空循环新增元素:循环add频繁触发扩容,提前指定容量或批量add。
-
大数据量禁用无界集合:无界队列、无参HashMap无限扩容,极易引发OOM。
-
集合用完手动缩容:ArrayList调用trimToSize(),释放冗余空闲数组空间。
-
并发场景优先CHM:ConcurrentHashMap并发扩容,性能远优于普通HashMap加锁。
-
ArrayList:1.5倍
-
Vector:2倍
-
HashMap、HashSet:2倍
-
负载因子越大:冲突多、省空间
-
负载因子越小:扩容快、浪费空间
第十二篇 生产环境选型(直接背诵)
-
普通查询、遍历 → ArrayList
-
频繁头尾增删 → LinkedList、ArrayDeque
-
去重无序 → HashSet
-
去重有序 → LinkedHashSet
-
自动排序 → TreeSet、TreeMap
-
普通键值 → HashMap
-
高并发键值 → ConcurrentHashMap
-
读多写少 → CopyOnWriteArrayList
-
延时任务 → DelayQueue
-
临时缓存自动回收 → WeakHashMap
第十三篇 编码黄金规约
-
初始化集合必须指定容量,减少扩容
-
自定义对象存哈希集合必须重写hashCode+equals
-
禁止可变对象作为HashMap key
-
多线程严禁使用ArrayList、HashMap
-
遍历不要增删,避免fast-fail
-
判断空集合使用 isEmpty()
-
大批量数据分批处理,防止OOM
-
禁止使用Executors快速创建线程池
第十四篇 集合100道面试真题(标准答案)
第一部分 基础(1-15)
-
Java集合分为哪两大类? 答:单列Collection、双列Map;Collection存单个元素,Map存键值对。
-
Collection和Collections区别? 答:Collection是根接口;Collections是工具类。
-
List、Set、Queue区别? 答:List有序可重复;Set无序不可重复;Queue先进先出。
-
迭代器作用? 答:统一遍历方式,支持删除。
-
foreach原理? 答:底层迭代器。
-
fail-fast原理? 答:modCount修改次数不一致抛异常。
-
fail-safe原理? 答:遍历快照,读写分离。
-
isEmpty和size区别? 答:isEmpty效率更高。
-
数组和集合区别? 答:数组长度固定;集合可变。
-
泛型作用? 答:类型安全、避免强转。
-
Iterable和Iterator区别? 答:Iterable获取迭代器,Iterator执行遍历。
-
ListIterator特点? 答:双向遍历、可修改。
-
同步集合和并发集合区别? 答:同步全局锁;并发细粒度锁。
-
不可变集合特点? 答:不可增删、线程安全。
-
asList坑? 答:固定大小、不能add、基本类型数组异常。
第二部分 List(16-35)
-
ArrayList底层? 答:动态Object数组,懒加载。
-
扩容机制? 答:1.5倍扩容,数组拷贝。
-
为什么默认空数组? 答:懒加载节省内存。
-
最大容量? 答:Integer.MAX_VALUE-8。
-
查询快增删慢原因? 答:连续内存、中间移动元素。
-
LinkedList底层? 答:双向链表。
-
LinkedList优缺点? 答:头尾快、查询慢。
-
两者使用场景? 答:查询多ArrayList;增删多LinkedList。
-
Vector特点? 答:扩容2倍、加锁、淘汰。
-
Stack为什么不用? 答:性能差,推荐ArrayDeque。
-
三种线程安全List? 答:Vector、synchronizedList、CopyOnWriteArrayList。
-
CopyOnWrite原理? 答:写时复制、读写分离。
-
缺点? 答:内存大、弱一致性。
-
ArrayList线程不安全表现? 答:并发add覆盖、越界。
-
指定容量好处? 答:减少扩容。
-
遍历删除注意? 答:迭代器删除、禁止foreach删除。
-
去重方式? 答:LinkedHashSet、Stream。
-
批量添加优化? 答:提前初始化容量。
-
ArrayDeque区别? 答:双端队列,替代栈。
-
List排序? 答:Collections.sort、Stream排序。
第三部分 Set(36-55)
-
Set特点? 答:无序、不可重复、无索引。
-
HashSet底层? 答:封装HashMap。
-
去重原理? 答:先hashCode后equals。
-
自定义对象必须重写? 答:必须重写两个方法,否则去重失效。
-
LinkedHashSet特点? 答:保留插入顺序。
-
TreeSet底层? 答:红黑树、TreeMap。
-
排序方式? 答:Comparable、Comparator。
-
去重依据? 答:compareTo返回0。
-
Set选型? 答:无序HashSet、有序Linked、排序Tree。
-
EnumSet特点? 答:枚举专用、性能最高。
-
Set允许null? 答:HashSet允许一个,TreeSet不允许。
-
TreeSet禁止null原因? 答:比较器空指针。
-
HashSet线程安全? 答:不安全。
-
有序实现? 答:LinkedHashSet。
-
默认参数? 答:16、0.75、扩容2倍。
-
0.75优势? 答:平衡冲突与空间。
-
负载因子影响? 答:越大冲突多、越小浪费空间。
-
遍历方式? 答:迭代器、foreach、Stream。
-
Set转List? 答:new ArrayList<>(set)。
-
排序优先级? 答:Comparator高于Comparable。
第四部分 Queue(56-70)
-
队列特点? 答:先进先出。
-
ArrayDeque特点? 答:数组双端队列、不能存null。
-
PriorityQueue原理? 答:最小堆。
-
阻塞队列作用? 答:生产者消费者、自动阻塞唤醒。
-
七大阻塞队列? 答:Array、Linked、Synchronous、Delay、Priority、Transfer、LinkedDeque。
-
Array与Linked队列区别? 答:单锁vs双锁、有界vs无界。
-
SynchronousQueue特点? 答:无容量、一对一传递。
-
DelayQueue场景? 答:订单超时、延时任务。
-
四种API? 答:报错、布尔、阻塞、超时。
-
双向队列场景? 答:头尾灵活存取。
-
阻塞非阻塞区别? 答:满空策略不同。
-
线程池常用队列? 答:Linked、Array、Synchronous。
-
PriorityBlockingQueue? 答:无界优先级阻塞队列。
-
队列实现栈? 答:两个队列互倒。
-
栈实现队列? 答:入栈出栈分离。
第五部分 Map(71-90)
-
Map结构? 答:键值对、key唯一。
-
HashMap1.7与1.8区别? 答:头插改尾插、加入红黑树、优化哈希。
-
核心参数? 答:16、0.75、8、6、64。
-
put流程? 答:哈希→下标→判断冲突→链表/红黑树→扩容。
-
扰动函数作用? 答:高位参与运算、减少冲突。
-
为什么(n-1)&hash? 答:高效位运算、代替取模。
-
容量必须2的幂? 答:散列均匀、扩容方便。
-
扩容流程? 答:扩容2倍、迁移到两个位置。
-
1.7死循环原因? 答:头插法并发循环链表。
-
null键null值? 答:允许一个null键。
-
LinkedHashMap特点? 答:有序、可实现LRU。
-
TreeMap原理? 答:红黑树、排序。
-
HashTable淘汰原因? 答:全局锁、性能差。
-
Properties作用? 答:读取配置文件。
-
WeakHashMap原理? 答:弱引用、自动回收。
-
JDK1.7ConcurrentHashMap? 答:分段锁。
-
JDK1.8ConcurrentHashMap? 答:CAS+Synchronized、锁首节点。
-
为什么禁止null? 答:并发无法辨别空。
-
Map遍历效率? 答:entrySet最高。
-
HashMap线程安全方案? 答:ConcurrentHashMap、同步Map。
第六部分 高阶(91-100)
-
大量数据HashMap优化? 答:初始化容量=预估/0.75+1。
-
树化条件? 答:链表≥8、数组≥64。
-
降级条件? 答:≤6。
-
LRU实现? 答:LinkedHashMap重写淘汰方法。
-
集合内存泄漏原因? 答:静态集合、ThreadLocal、强引用滞留。
-
多线程规范? 答:读多写少COW、并发Map、阻塞队列。
-
Stream常用操作? 答:过滤、分组、排序、映射、去重。
-
大数据优化? 答:分批、分页、不一次性加载。
-
JDK8集合新特性? 答:Stream、removeIf、merge。
-
集合终极选型总结? 答:普通ArrayList、去重HashSet、并发JUC、队列阻塞队列。
第十五篇 面试一句话终极背诵
-
List:ArrayList查快改慢、LinkedList增删快
-
Set:Hash无序、Linked有序、Tree排序
-
Map:HashMap日常、ConcurrentHashMap并发、TreeMap排序
-
并发:读多写少COW、键值并发CHM、生产消费阻塞队列
-
底层:数组查询快、链表增删快、红黑树平衡、哈希表最快
-
禁忌:不可变key、禁止无限扩容、禁止循环创建集合
第十六篇 源码高频灵魂面试题(压轴)
-
ArrayList扩容为什么是1.5倍? 答:平衡扩容次数与内存浪费,1.5倍是黄金比例。
-
HashMap容量为什么必须是2的幂? 答:位运算替代取模、散列均匀、扩容迁移简单。
-
负载因子为什么是0.75? 答:泊松分布,平衡哈希冲突与空间利用率。
-
为什么链表超过8转红黑树? 答:链表长度达到8概率极低,减少转换开销。
-
ConcurrentHashMap为什么放弃分段锁? 答:节点锁粒度更细、CAS无锁、并发性能爆炸提升。
-
HashMap为什么不使用纯红黑树? 答:短链表占用内存更小、创建开销更低、查询更快。
-
为什么重写equals必须重写hashCode? 答:保证相同对象哈希一致,否则哈希集合永久去重失败。