Java集合大全终极手册(一)

前言

本文汇总全网最全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版本迭代集合重大变更(面试必问)

  1. JDK1.5:引入泛型、Iterable、增强for循环,彻底告别裸集合

  2. JDK1.7 :ArrayList、HashMap改为懒加载,节约初始化内存;LinkedList取消循环链表

  3. JDK1.8:HashMap加入红黑树、ConcurrentHashMap放弃分段锁、新增Stream流、Lambda表达式

  4. JDK1.9:of()快速创建不可变集合,禁止add/remove

  5. JDK11:ArrayList新增trimToSize()缩容方法

  6. JDK17:集合底层优化、垃圾回收优化、弱化synchronized锁偏向

1.4.6 泛型擦除(集合底层原理)

  • 本质:泛型只在编译期生效,运行期全部擦除为Object类型

  • 原理:编译器校验类型,生成字节码时去除泛型标记

  • 弊端:无法使用基本类型、无法获取泛型Class、泛型数组创建失败

  • 拓展:通配符 ?、? extends 上边界、? super 下边界

1.4.7 不可变集合(冷门高频面试)

1. 三种创建方式

  1. JDK9+ List.of():最简单、线程安全、底层定长数组

  2. Collections.unmodifiableList():包装原有集合,动态不可变

  3. 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:数据覆盖、数组越界异常

  1. 数据覆盖原因elementData[size++] 非原子操作;分为取值、赋值、自增三步,多线程同时赋值会覆盖。

  2. 数组越界原因:多个线程同时校验扩容,判定不需要扩容,最终元素超出数组长度抛出异常。

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 高频刁钻面试题(压轴)

  1. 为什么无参构造初始为空数组,不是10? 答:懒加载设计,大量空集合节省堆内存,首次add才开辟容量为10的数组。

  2. 为什么扩容是右移一位(1.5倍)? 答:位运算效率极高、1.5倍属于黄金比例,平衡扩容次数与内存浪费。

  3. 为什么elementData要transient修饰? 答:底层数组存在大量空冗余位置,手动序列化只保存有效元素,节省IO流量。

  4. 最大容量为什么是Integer.MAX_VALUE - 8? 答:防止部分虚拟机对象头占用内存,避免内存溢出报错。

  5. 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 高频面试刁钻题

  1. 为什么LinkedList不实现RandomAccess? 答:底层双向链表,内存分散,无法高效随机下标访问,规避错误遍历写法,提醒开发者禁止普通for循环遍历。

  2. JDK1.7为什么取消循环链表? 答:简化头尾节点操作逻辑、减少指针冗余、降低代码复杂度,避免循环指针造成内存遍历死循环。

  3. LinkedList能否存储null? 答:可以,无任何非空校验,允许存储多个null元素。

  4. 为什么不能用普通for循环遍历? 答:每次get(index)都会触发二分遍历,循环嵌套遍历时间复杂度退化至O(n²),数据量大直接卡顿。

  5. 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去重五种方式(生产常用)
  1. LinkedHashSet去重:保留插入顺序,最简单通用

  2. Stream流去重:代码简洁,JDK8+推荐

  3. 循环比对去重:无需依赖其他集合,效率低

  4. TreeSet去重:去重+自动排序

  5. 自定义规则去重: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 去重原理(源码深挖)

  1. 先判断hashCode()

  2. 哈希相同再判断equals()

  3. 全部相同判定重复

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 高频面试刁钻题
  1. HashSet 为什么无序? 答:底层哈希表,元素按照哈希下标散列存储,不保留插入顺序。

  2. HashSet 增删查时间复杂度? 答:理想O(1),哈希冲突退化后链表O(n)、树化后O(logn)。

  3. HashSet 线程安全吗? 答:不安全,底层依托HashMap,并发增删会出现数据覆盖、丢失。

  4. 怎么实现线程安全的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 两种顺序模式(必考区别)
  1. 插入顺序(默认)accessOrder=false 元素顺序永远等于添加顺序,修改、查询元素不会改变顺序,业务最常用。

  2. 访问顺序 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 高频刁钻面试题
  1. LinkedHashSet 怎么实现顺序? 答:底层LinkedHashMap维护双向链表,记录元素插入先后位置。

  2. 和HashSet遍历谁更快? 答:LinkedHashSet更快,链表有序连续遍历,无需哈希寻址。

  3. 能否实现LRU缓存? 答:可以,反射修改accessOrder为true,开启访问排序。

  4. 为什么不自动扩容保持链表顺序? 答:扩容重建哈希表,双向链表顺序不会打乱,底层保留引用关系。

  5. 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 高频面试题

  1. TreeSet 为什么不能存null? 答:底层比较器需要调用比较方法,null无引用,直接抛出空指针异常。

  2. TreeSet 去重规则和HashSet区别? 答:HashSet:hashCode+equals;TreeSet:compareTo返回0。

  3. 两种排序优先级? 答:定制排序(Comparator) > 自然排序(Comparable)。

  4. 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底层集合可提前初始化容量,减少扩容

  1. 先判断hashCode()

  2. 哈希相同再判断equals()

  3. 全部相同判定重复

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 堆排序核心规则
  1. 默认自然排序:数值从小到大、字典序排序,构建最小堆

  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 阻塞队列高频灵魂面试题

  1. Array和Linked阻塞队列最大区别? 答:数组单锁、链表双锁;数组有界、链表默认无界;链表吞吐量更高。

  2. SynchronousQueue为什么无容量? 答:为了实现一对一即时传递,不缓存任务,控制线程并发数量。

  3. DelayQueue怎么实现延时? 答:底层最小堆排序,头部为最早到期元素,阻塞等待到期唤醒。

  4. 阻塞队列为什么不允许null? 答:null作为特殊返回值,用于判断队列获取元素失败,防止逻辑混淆。

  5. 双锁为什么比单锁性能高? 答:读写分离,生产者写队列、消费者读队列互不竞争锁资源。

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 红黑树 树化&降级 完整规则

✅ 树化触发条件(必须同时满足)
  1. 单链表节点长度 ≥8

  2. 哈希表数组容量 ≥64

❌ 不满足树化条件

数组容量<64,链表过长仅扩容、不树化,优先扩大散列空间减少冲突

✅ 树降级(链表化)触发条件
  1. 扩容迁移后,树节点数量**≤6**,自动降级为普通链表

  2. 阈值预留7、8作为缓冲,防止频繁树化降级、抖动消耗性能

红黑树五大特性(面试背诵)
  1. 节点只有红色、黑色两种颜色

  2. 根节点永久为黑色

  3. 所有叶子节点(空节点)为黑色

  4. 红色节点的子节点必须为黑色(不能连续红节点)

  5. 任意节点到所有叶子节点,黑色节点数量相同(黑色平衡)

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 灵魂高频面试题(压轴刁钻)

  1. 为什么哈希表容量必须是2的幂次? 答:①位运算替代取模,计算下标速度极快;②扩容高低位拆分迁移逻辑简单;③保证哈希散列均匀,减少冲突。

  2. 扰动函数为什么要高低位异或? 答:日常key哈希值高位变化大、低位变化小,高低位混合运算,让低位携带高位特征,减少低位哈希冲突。

  3. 为什么树化阈值是8,降级是6? 答:泊松分布统计,链表长度达到8概率极低;预留中间缓冲区间,防止频繁树化降级、性能抖动。

  4. HashMap为什么不直接用红黑树? 答:链表节点占用内存更小、创建开销低;短链表查询速度快;红黑树维护平衡消耗资源,适合长链表。

  5. 负载因子0.75能否修改?生产建议? 答:可以修改;负载因子越大内存利用率高、冲突多;越小扩容频繁、浪费内存;生产禁止修改,0.75为官方最优平衡值。

  6. 为什么扩容后不需要重新计算哈希? 答:仅判断旧容量二进制高位,0留原位、1移高位,无需重复哈希运算,大幅优化扩容效率。

  7. 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 高频刁钻面试题(背诵版)

  1. LinkedHashMap 为什么有序? 答:底层维护全局双向链表,所有插入/访问的节点通过before、after指针串联,遍历遵循链表顺序。

  2. 和HashMap遍历区别? 答:HashMap遍历无序;LinkedHashMap遍历严格遵循插入/访问顺序。

  3. accessOrder两种模式业务场景? 答:false插入顺序:有序字典、接口有序返回;true访问顺序:本地LRU缓存、热点数据留存。

  4. 为什么不重写put方法? 答:复用HashMap哈希、扩容、树化逻辑,仅通过后置回调维护链表,简化代码、减少冗余。

  5. 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(冷门必考)

  1. WeakHashMap:key弱引用,GC自动回收,防内存泄漏

  2. IdentityHashMap:==判断内存地址,不重写equals

  3. 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 高频刁钻面试题(压轴背诵)

  1. 为什么JDK1.8放弃ReentrantLock改用synchronized? 答:①synchronized经过虚拟机深度优化,性能反超;②锁粒度细化到桶节点,竞争极低;③底层无额外对象开销,内存更省。

  2. 并发扩容会不会数据丢失? 答:不会,迁移前标记MOVED状态,线程互斥迁移,高低位拆分无覆盖。

  3. ConcurrentHashMap 支持排序吗? 答:不支持,底层无序;并发有序使用ConcurrentSkipListMap(跳表)。

  4. 为什么红黑树要用自旋锁? 答:树节点修改耗时短、频率低,自旋无阻塞,比重量级锁开销更小。

  5. 高并发大量哈希冲突怎么优化? 答:重写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并发集合四大分类:

  1. 并发List:CopyOnWriteArrayList、CopyOnWriteArraySet

  2. 并发Map:ConcurrentHashMap、ConcurrentSkipListMap

  3. 并发Set:ConcurrentSkipListSet、CopyOnWriteArraySet

  4. 阻塞队列(核心重点):七大阻塞队列,线程池底层依赖

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 面试高频题
  1. 为什么并发有序不用TreeMap? 答:TreeMap非线程安全,加锁后性能极差;跳表无锁并发,有序且吞吐量更高。

  2. 跳表对比红黑树优势? 答:结构简单、无旋转平衡开销、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并发集合高频面试刁钻题

  1. CopyOnWriteArrayList 为什么弱一致性? 答:遍历读取旧数组快照,写入生成新数组,遍历期间无法感知最新数据,适合不要求实时性的读多写少场景。

  2. 阻塞队列为什么禁止存储null? 答:null作为特殊标记,用于判断队列读取终止,存储null会混淆空数据与终止标记。

  3. Array和Linked阻塞队列锁区别? 答:Array单锁读写互斥;Linked双锁读写分离,互不阻塞、吞吐量更高。

  4. SynchronousQueue为什么无容量? 答:设计初衷为线程直连传递,不缓存任务,避免任务堆积,适合高频短任务。

  5. 并发有序集合怎么选型? 答:少量有序用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() 五大坑

  1. 返回内部类,不是ArrayList

  2. 不能add/remove

  3. 基本类型数组被当成一个元素

  4. 修改元素影响原数组

  5. 无扩容方法

第九篇章 内存泄漏(架构师必懂)

  1. static静态集合:常驻内存永不回收

  2. ThreadLocalMap:key弱引用、value强引用,不remove泄漏

  3. 自定义可变对象作为HashMap key,属性修改内存泄漏

  4. 长生命周期集合持有短生命周期对象

  5. 双括号初始化:匿名内部类持有外部引用

第十篇章 Stream流式编程完整版(源码深挖+生产实战+避坑指南)

10.1 Stream核心概述(底层定位)

Stream是JDK8重磅新增 的集合处理API,基于Lambda表达式,采用流水线思想处理集合、数组数据,摒弃传统循环遍历的冗余代码。Stream不存储数据、不修改原数据源、延迟执行,仅对数据做计算、转换、筛选、归并操作,是生产环境代码极简优化的核心工具。

10.1.1 四大核心特性(面试必背)

  • 无存储:Stream不是数据容器,不保存元素,仅对流式传输的数据做处理,数据源为集合、数组、IO流。

  • 不可变:所有操作不会修改原数据源,处理后生成新流/新集合,无副作用。

  • 延迟执行:中间操作仅封装执行逻辑,无实际运算;触发终止操作才会执行全部流水线。

  • 链式编程:方法链式调用,代码简洁紧凑,可读性极强。

10.1.2 Stream核心组成架构

完整流水线分为三步,缺一不可:

  1. 创建流(源头):获取数据源对应的Stream流

  2. 中间操作(加工):筛选、映射、排序、去重等,返回新流,可链式叠加

  3. 终止操作(收尾):触发执行、收集结果、遍历统计,执行后流关闭不可复用

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汇总)

  1. 流只能使用一次 错误:重复调用终止操作;纠正:流水线一次性执行,禁止复用流对象。

  2. toMap键重复报错 问题:key重复抛IllegalStateException;纠正:第四个参数设置合并规则 (v1,v2)->v1。

  3. 并行流修改共享变量 问题:多线程并发修改,数据覆盖错乱;纠正:并行流保持无状态,不修改外部变量。

  4. distinct去重自定义对象失效 原因:未重写equals+hashCode;纠正:哈希集合、Stream去重必须重写双方法。

  5. peek修改不可变对象无效 String、Integer不可变类型,peek修改不会生效,仅支持自定义引用类型修改属性。

  6. 空集合直接get()报错 findFirst().get() 无元素抛空指针;纠正:使用orElse、orElseGet设置默认值。

10.7 Stream生产级编码规约(大厂规范)

  • 优先使用链式写法,代码扁平化,禁止拆分零散定义流对象。

  • 筛选操作前置:filter、skip、limit尽量靠前,减少后续运算数据量。

  • 排序、去重后置:有状态操作放在无状态操作之后,减少缓存开销。

  • Map转换优先指定泛型,避免toMap泛型擦除,防止类型转换异常。

  • 空元素防护:使用Optional包装元素,杜绝空指针异常。

  • 大数据量分批处理,并行流仅用于CPU密集型、无状态计算任务。

  • 禁止在Stream中写复杂业务逻辑,Lambda保持极简,提高可读性。

10.8 Stream高频面试刁钻题(压轴)

  1. Stream为什么延迟执行? 答:中间操作封装逻辑不运算,终止操作一次性执行,减少循环次数、提升执行效率,合并多次遍历为一次。

  2. Stream和for循环哪个快? 答:少量数据for循环更快;中等数据串行流持平;大数据量并行流碾压for循环。

  3. 并行流为什么不推荐业务使用? 答:顺序不可控、线程不安全、公共线程池容易被占用,影响全局线程调度。

  4. peek和map区别? 答:peek无返回值,修改原有对象属性;map有返回值,做类型转换、生成新元素。

  5. 为什么流不能重复使用? 答:流内部标记执行状态,终止操作执行后标记关闭,防止重复运算、数据错乱。

第十一篇 集合扩容机制全网汇总(零遗漏完整版)

扩容是集合核心底层机制,所有可变集合初始化时不会开辟大量内存,在元素达到阈值后自动扩容,本篇汇总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 负载因子深度剖析(面试必考)

  1. 负载因子定义:衡量集合数组填充密集程度,公式:负载因子=元素个数/数组容量

  2. 默认0.75原因 :依据泊松分布,哈希冲突概率最低,完美平衡内存占用查询效率

  3. 负载因子偏大(如1.0):数组填满才扩容,节省内存;哈希冲突激增,链表变长,查询效率暴跌

  4. 负载因子偏小(如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 生产扩容优化规约(大厂规范)

  1. 预估数据初始化容量 :HashMap初始化容量计算公式:预估元素个数 / 0.75 + 1,规避自动扩容。

  2. 禁止空循环新增元素:循环add频繁触发扩容,提前指定容量或批量add。

  3. 大数据量禁用无界集合:无界队列、无参HashMap无限扩容,极易引发OOM。

  4. 集合用完手动缩容:ArrayList调用trimToSize(),释放冗余空闲数组空间。

  5. 并发场景优先CHM:ConcurrentHashMap并发扩容,性能远优于普通HashMap加锁。

  6. ArrayList:1.5倍

  7. Vector:2倍

  8. HashMap、HashSet:2倍

  9. 负载因子越大:冲突多、省空间

  10. 负载因子越小:扩容快、浪费空间

第十二篇 生产环境选型(直接背诵)

  1. 普通查询、遍历 → ArrayList

  2. 频繁头尾增删 → LinkedList、ArrayDeque

  3. 去重无序 → HashSet

  4. 去重有序 → LinkedHashSet

  5. 自动排序 → TreeSet、TreeMap

  6. 普通键值 → HashMap

  7. 高并发键值 → ConcurrentHashMap

  8. 读多写少 → CopyOnWriteArrayList

  9. 延时任务 → DelayQueue

  10. 临时缓存自动回收 → WeakHashMap

第十三篇 编码黄金规约

  1. 初始化集合必须指定容量,减少扩容

  2. 自定义对象存哈希集合必须重写hashCode+equals

  3. 禁止可变对象作为HashMap key

  4. 多线程严禁使用ArrayList、HashMap

  5. 遍历不要增删,避免fast-fail

  6. 判断空集合使用 isEmpty()

  7. 大批量数据分批处理,防止OOM

  8. 禁止使用Executors快速创建线程池

第十四篇 集合100道面试真题(标准答案)

第一部分 基础(1-15)

  1. Java集合分为哪两大类? 答:单列Collection、双列Map;Collection存单个元素,Map存键值对。

  2. Collection和Collections区别? 答:Collection是根接口;Collections是工具类。

  3. List、Set、Queue区别? 答:List有序可重复;Set无序不可重复;Queue先进先出。

  4. 迭代器作用? 答:统一遍历方式,支持删除。

  5. foreach原理? 答:底层迭代器。

  6. fail-fast原理? 答:modCount修改次数不一致抛异常。

  7. fail-safe原理? 答:遍历快照,读写分离。

  8. isEmpty和size区别? 答:isEmpty效率更高。

  9. 数组和集合区别? 答:数组长度固定;集合可变。

  10. 泛型作用? 答:类型安全、避免强转。

  11. Iterable和Iterator区别? 答:Iterable获取迭代器,Iterator执行遍历。

  12. ListIterator特点? 答:双向遍历、可修改。

  13. 同步集合和并发集合区别? 答:同步全局锁;并发细粒度锁。

  14. 不可变集合特点? 答:不可增删、线程安全。

  15. asList坑? 答:固定大小、不能add、基本类型数组异常。

第二部分 List(16-35)

  1. ArrayList底层? 答:动态Object数组,懒加载。

  2. 扩容机制? 答:1.5倍扩容,数组拷贝。

  3. 为什么默认空数组? 答:懒加载节省内存。

  4. 最大容量? 答:Integer.MAX_VALUE-8。

  5. 查询快增删慢原因? 答:连续内存、中间移动元素。

  6. LinkedList底层? 答:双向链表。

  7. LinkedList优缺点? 答:头尾快、查询慢。

  8. 两者使用场景? 答:查询多ArrayList;增删多LinkedList。

  9. Vector特点? 答:扩容2倍、加锁、淘汰。

  10. Stack为什么不用? 答:性能差,推荐ArrayDeque。

  11. 三种线程安全List? 答:Vector、synchronizedList、CopyOnWriteArrayList。

  12. CopyOnWrite原理? 答:写时复制、读写分离。

  13. 缺点? 答:内存大、弱一致性。

  14. ArrayList线程不安全表现? 答:并发add覆盖、越界。

  15. 指定容量好处? 答:减少扩容。

  16. 遍历删除注意? 答:迭代器删除、禁止foreach删除。

  17. 去重方式? 答:LinkedHashSet、Stream。

  18. 批量添加优化? 答:提前初始化容量。

  19. ArrayDeque区别? 答:双端队列,替代栈。

  20. List排序? 答:Collections.sort、Stream排序。

第三部分 Set(36-55)

  1. Set特点? 答:无序、不可重复、无索引。

  2. HashSet底层? 答:封装HashMap。

  3. 去重原理? 答:先hashCode后equals。

  4. 自定义对象必须重写? 答:必须重写两个方法,否则去重失效。

  5. LinkedHashSet特点? 答:保留插入顺序。

  6. TreeSet底层? 答:红黑树、TreeMap。

  7. 排序方式? 答:Comparable、Comparator。

  8. 去重依据? 答:compareTo返回0。

  9. Set选型? 答:无序HashSet、有序Linked、排序Tree。

  10. EnumSet特点? 答:枚举专用、性能最高。

  11. Set允许null? 答:HashSet允许一个,TreeSet不允许。

  12. TreeSet禁止null原因? 答:比较器空指针。

  13. HashSet线程安全? 答:不安全。

  14. 有序实现? 答:LinkedHashSet。

  15. 默认参数? 答:16、0.75、扩容2倍。

  16. 0.75优势? 答:平衡冲突与空间。

  17. 负载因子影响? 答:越大冲突多、越小浪费空间。

  18. 遍历方式? 答:迭代器、foreach、Stream。

  19. Set转List? 答:new ArrayList<>(set)。

  20. 排序优先级? 答:Comparator高于Comparable。

第四部分 Queue(56-70)

  1. 队列特点? 答:先进先出。

  2. ArrayDeque特点? 答:数组双端队列、不能存null。

  3. PriorityQueue原理? 答:最小堆。

  4. 阻塞队列作用? 答:生产者消费者、自动阻塞唤醒。

  5. 七大阻塞队列? 答:Array、Linked、Synchronous、Delay、Priority、Transfer、LinkedDeque。

  6. Array与Linked队列区别? 答:单锁vs双锁、有界vs无界。

  7. SynchronousQueue特点? 答:无容量、一对一传递。

  8. DelayQueue场景? 答:订单超时、延时任务。

  9. 四种API? 答:报错、布尔、阻塞、超时。

  10. 双向队列场景? 答:头尾灵活存取。

  11. 阻塞非阻塞区别? 答:满空策略不同。

  12. 线程池常用队列? 答:Linked、Array、Synchronous。

  13. PriorityBlockingQueue? 答:无界优先级阻塞队列。

  14. 队列实现栈? 答:两个队列互倒。

  15. 栈实现队列? 答:入栈出栈分离。

第五部分 Map(71-90)

  1. Map结构? 答:键值对、key唯一。

  2. HashMap1.7与1.8区别? 答:头插改尾插、加入红黑树、优化哈希。

  3. 核心参数? 答:16、0.75、8、6、64。

  4. put流程? 答:哈希→下标→判断冲突→链表/红黑树→扩容。

  5. 扰动函数作用? 答:高位参与运算、减少冲突。

  6. 为什么(n-1)&hash? 答:高效位运算、代替取模。

  7. 容量必须2的幂? 答:散列均匀、扩容方便。

  8. 扩容流程? 答:扩容2倍、迁移到两个位置。

  9. 1.7死循环原因? 答:头插法并发循环链表。

  10. null键null值? 答:允许一个null键。

  11. LinkedHashMap特点? 答:有序、可实现LRU。

  12. TreeMap原理? 答:红黑树、排序。

  13. HashTable淘汰原因? 答:全局锁、性能差。

  14. Properties作用? 答:读取配置文件。

  15. WeakHashMap原理? 答:弱引用、自动回收。

  16. JDK1.7ConcurrentHashMap? 答:分段锁。

  17. JDK1.8ConcurrentHashMap? 答:CAS+Synchronized、锁首节点。

  18. 为什么禁止null? 答:并发无法辨别空。

  19. Map遍历效率? 答:entrySet最高。

  20. HashMap线程安全方案? 答:ConcurrentHashMap、同步Map。

第六部分 高阶(91-100)

  1. 大量数据HashMap优化? 答:初始化容量=预估/0.75+1。

  2. 树化条件? 答:链表≥8、数组≥64。

  3. 降级条件? 答:≤6。

  4. LRU实现? 答:LinkedHashMap重写淘汰方法。

  5. 集合内存泄漏原因? 答:静态集合、ThreadLocal、强引用滞留。

  6. 多线程规范? 答:读多写少COW、并发Map、阻塞队列。

  7. Stream常用操作? 答:过滤、分组、排序、映射、去重。

  8. 大数据优化? 答:分批、分页、不一次性加载。

  9. JDK8集合新特性? 答:Stream、removeIf、merge。

  10. 集合终极选型总结? 答:普通ArrayList、去重HashSet、并发JUC、队列阻塞队列。

第十五篇 面试一句话终极背诵

  1. List:ArrayList查快改慢、LinkedList增删快

  2. Set:Hash无序、Linked有序、Tree排序

  3. Map:HashMap日常、ConcurrentHashMap并发、TreeMap排序

  4. 并发:读多写少COW、键值并发CHM、生产消费阻塞队列

  5. 底层:数组查询快、链表增删快、红黑树平衡、哈希表最快

  6. 禁忌:不可变key、禁止无限扩容、禁止循环创建集合

第十六篇 源码高频灵魂面试题(压轴)

  1. ArrayList扩容为什么是1.5倍? 答:平衡扩容次数与内存浪费,1.5倍是黄金比例。

  2. HashMap容量为什么必须是2的幂? 答:位运算替代取模、散列均匀、扩容迁移简单。

  3. 负载因子为什么是0.75? 答:泊松分布,平衡哈希冲突与空间利用率。

  4. 为什么链表超过8转红黑树? 答:链表长度达到8概率极低,减少转换开销。

  5. ConcurrentHashMap为什么放弃分段锁? 答:节点锁粒度更细、CAS无锁、并发性能爆炸提升。

  6. HashMap为什么不使用纯红黑树? 答:短链表占用内存更小、创建开销更低、查询更快。

  7. 为什么重写equals必须重写hashCode? 答:保证相同对象哈希一致,否则哈希集合永久去重失败。

相关推荐
信竞星球_少儿编程题库4 小时前
2026年全国信息素养大赛算法应用主题赛 丝路新城 C++ 模拟卷(三)
开发语言·c++
Cosolar4 小时前
吃透 Spring Cloud Gateway:基于 Spring Boot 3 的核心原理、企业级实战与避坑指南
java·spring cloud·架构
千里马-horse4 小时前
gRPC -- Java 基础教程
java·开发语言·grpc
甲方大人请饶命4 小时前
Java-面向对象进阶(qqbb知识点)
java·开发语言
ChoSeitaku4 小时前
07_static_JavaBean_继承_super/this
java·开发语言
江南十四行4 小时前
并发编程(一)
java·jvm·算法
Dicky-_-zhang4 小时前
自动化运维实战:监控告警与自动化运维的完整方案
java·jvm
hbugs0014 小时前
EVE-NG桥接外网的5种方式
开发语言·网络·php·eve-ng·rstp·流量洞察
wjs20245 小时前
Lua 字符串
开发语言