14.Java 集合框架从入门到源码万字详解(含 ArrayList/LinkedList/HashMap 源码、泛型通配符、红黑树)

本文整理自学习笔记,覆盖 Java 集合框架核心知识点:Collection/List/Set/Map 全家桶、ArrayList 与 LinkedList 源码、HashMap/TreeMap 源码、泛型与通配符 PECS、红黑树原理、Collections 工具类等


目录

一、集合快速选择指南

[1.1 五大常用集合一句话选择](#1.1 五大常用集合一句话选择)

[1.2 记忆口诀](#1.2 记忆口诀)

[1.3 详细对比](#1.3 详细对比)

[1.4 性能复杂度表](#1.4 性能复杂度表)

[1.5 选择决策树](#1.5 选择决策树)

[二、Collection 接口](#二、Collection 接口)

[2.1 一句话理解](#2.1 一句话理解)

[2.2 常用方法(必背)](#2.2 常用方法(必背))

三、迭代器与遍历方式

[3.1 Iterator 迭代器](#3.1 Iterator 迭代器)

[3.2 增强 for 循环(foreach)](#3.2 增强 for 循环(foreach))

[3.3 Lambda forEach(Java 8+)](#3.3 Lambda forEach(Java 8+))

[3.4 四种遍历方式选择](#3.4 四种遍历方式选择)

[四、List 体系与源码](#四、List 体系与源码)

[4.1 List 独有方法与五种遍历](#4.1 List 独有方法与五种遍历)

[4.2 ArrayList 源码逐行解析](#4.2 ArrayList 源码逐行解析)

[4.2.1 类声明与核心字段](#4.2.1 类声明与核心字段)

[4.2.2 构造方法](#4.2.2 构造方法)

[4.2.3 add(E e) 方法](#4.2.3 add(E e) 方法)

[4.2.4 add(int index, E element) 指定位置插入](#4.2.4 add(int index, E element) 指定位置插入)

[4.2.5 get / remove / addAll](#4.2.5 get / remove / addAll)

[4.2.6 ArrayList 核心总结](#4.2.6 ArrayList 核心总结)

[4.3 LinkedList 源码逐行解析](#4.3 LinkedList 源码逐行解析)

[4.3.1 底层 Node 结构](#4.3.1 底层 Node 结构)

[4.3.2 添加元素](#4.3.2 添加元素)

[4.3.3 删除元素](#4.3.3 删除元素)

[4.3.4 getNode 的折半优化](#4.3.4 getNode 的折半优化)

[4.3.5 fail-fast 机制](#4.3.5 fail-fast 机制)

[4.3.6 LinkedList vs ArrayList](#4.3.6 LinkedList vs ArrayList)

五、常见数据结构基础

[5.1 栈(Stack,LIFO)](#5.1 栈(Stack,LIFO))

[5.2 队列(Queue,FIFO)](#5.2 队列(Queue,FIFO))

[5.3 数组 vs 链表](#5.3 数组 vs 链表)

[5.4 二叉树 / BST / AVL](#5.4 二叉树 / BST / AVL)

[5.5 复杂度总览](#5.5 复杂度总览)

[六、Set 体系](#六、Set 体系)

[6.1 HashSet 与 LinkedHashSet 底层原理](#6.1 HashSet 与 LinkedHashSet 底层原理)

[6.2 TreeSet 排序(Comparable 与 Comparator)](#6.2 TreeSet 排序(Comparable 与 Comparator))

方式一:自然排序(Comparable)

方式二:定制排序(Comparator)

[Comparable vs Comparator](#Comparable vs Comparator)

[七、泛型与通配符 PECS](#七、泛型与通配符 PECS)

[7.1 为什么要有泛型](#7.1 为什么要有泛型)

[7.2 泛型擦除](#7.2 泛型擦除)

[7.3 三种通配符](#7.3 三种通配符)

[7.4 实战示例:Animal / Cat / Dog 继承结构](#7.4 实战示例:Animal / Cat / Dog 继承结构)

[八、Map 体系](#八、Map 体系)

[8.1 Map 接口与常用 API](#8.1 Map 接口与常用 API)

[8.2 HashMap 基本使用](#8.2 HashMap 基本使用)

[8.3 HashMap 源码深度解析](#8.3 HashMap 源码深度解析)

[8.3.1 核心常量](#8.3.1 核心常量)

[8.3.2 扰动函数 hash()](#8.3.2 扰动函数 hash())

[8.3.3 索引计算](#8.3.3 索引计算)

[8.3.4 put 流程(putVal)](#8.3.4 put 流程(putVal))

[8.3.5 扩容 resize() 与 (hash & oldCap) 优化](#8.3.5 扩容 resize() 与 (hash & oldCap) 优化)

[8.3.6 为什么树化阈值是 8?](#8.3.6 为什么树化阈值是 8?)

[8.3.7 HashMap 线程不安全](#8.3.7 HashMap 线程不安全)

[8.4 LinkedHashMap 与 LRU](#8.4 LinkedHashMap 与 LRU)

[8.5 TreeMap 与范围查询](#8.5 TreeMap 与范围查询)

九、红黑树原理

[9.1 五条铁律(必背)](#9.1 五条铁律(必背))

[9.2 为什么能保证平衡](#9.2 为什么能保证平衡)

[9.3 插入修复](#9.3 插入修复)

[9.4 红黑树 vs AVL](#9.4 红黑树 vs AVL)

[十、JDK5 可变参数](#十、JDK5 可变参数)

[10.1 语法与本质](#10.1 语法与本质)

[10.2 重载优先级](#10.2 重载优先级)

[十一、Collections 工具类](#十一、Collections 工具类)

[11.1 常用方法](#11.1 常用方法)

十二、面试高频问题汇总



一、集合快速选择指南

学集合第一件事不是背 API,而是知道什么场景用什么集合

1.1 五大常用集合一句话选择

需求 推荐集合 底层结构 特点
元素可重复、查询多 ArrayList ⭐⭐⭐ 动态数组 查询 O(1),用得最多
增删操作多 LinkedList ⭐⭐ 双向链表 增删 O(1),查询 O(n)
元素去重、不关心顺序 HashSet ⭐⭐⭐ 哈希表 无序、O(1),用得最多
去重 + 保持插入顺序 LinkedHashSet ⭐⭐ 哈希表 + 双向链表 按插入顺序遍历
元素自动排序 TreeSet ⭐⭐ 红黑树 O(log n),支持范围查询

1.2 记忆口诀

要重复,用 ArrayList,数组查询快。 增删多,用 LinkedList,链表增删快。 要去重,用 HashSet,哈希无序快。 去重还要顺序,LinkedHashSet,双链表保序。 要排序,用 TreeSet,红黑树有序。

1.3 详细对比

List vs Set:

特性 List Set
元素重复 允许 不允许
元素顺序 有序(插入顺序) HashSet 无序,TreeSet 有序
常用实现 ArrayList、LinkedList HashSet、LinkedHashSet、TreeSet

ArrayList vs LinkedList:

特性 ArrayList LinkedList
底层结构 动态数组 双向链表
查询速度 快 O(1) 慢 O(n)
增删速度 慢 O(n) 快 O(1)(已知节点)
内存占用 多(需存前后指针)

HashSet vs LinkedHashSet vs TreeSet:

特性 HashSet LinkedHashSet TreeSet
去重
有序 ✅ 插入顺序 ✅ 排序
速度 最快 较快 较慢 O(log n)
底层 哈希表 哈希表 + 双链表 红黑树

1.4 性能复杂度表

操作 ArrayList LinkedList HashSet LinkedHashSet TreeSet
添加 O(1) 均摊 O(1) O(1) O(1) O(log n)
删除 O(n) O(1)* O(1) O(1) O(log n)
查询 O(1) O(n) O(1) O(1) O(log n)
遍历 O(n) O(n) O(n) O(n) O(n)

*已知节点位置时 O(1),需要遍历时 O(n)。

1.5 选择决策树

复制代码
需要有序吗?
├── 是 → 用 List
│   ├── 查询多? → ArrayList
│   └── 增删多? → LinkedList
└── 否 → 用 Set / Map
    └── 需要去重吗?
        ├── 需要排序吗?
        │   ├── 是 → TreeSet / TreeMap
        │   └── 否 → 需要保持插入顺序吗?
        │       ├── 是 → LinkedHashSet / LinkedHashMap
        │       └── 否 → HashSet / HashMap

二、Collection 接口

2.1 一句话理解

Collection 是 Java 单列集合的老祖宗接口,下面有两大儿子:

复制代码
Collection(老祖宗接口)
├── List(有序、可重复)→ ArrayList、LinkedList
└── Set(无序、不重复) → HashSet、LinkedHashSet、TreeSet

单列集合 vs 双列集合:

  • 单列:["苹果", "香蕉", "橘子"],一个一个的元素
  • 双列(Map):{"小明": 18, "小红": 20},一对一对的键值对

2.2 常用方法(必背)

方法 作用 大白话 返回值
add(E e) 添加元素 往集合里扔一个东西 boolean
remove(Object o) 删除元素 把某个东西拿走 boolean
contains(Object o) 是否包含 集合里有没有这个东西 boolean
isEmpty() 是否为空 集合是不是空的 boolean
size() 获取大小 集合里有几个元素 int
clear() 清空 把集合里的东西全倒掉 void
toArray() 转数组 把集合变成数组 Object\[\]
复制代码
Collection<String> c = new ArrayList<>();
c.add("苹果");
c.add("香蕉");
c.add("苹果");          // List 允许重复
System.out.println(c); // [苹果, 香蕉, 苹果]

c.remove("苹果");       // 只删第一个匹配的
System.out.println(c); // [香蕉, 苹果]

System.out.println(c.contains("苹果")); // true
System.out.println(c.size());           // 2
c.clear();
System.out.println(c.isEmpty());         // true

三、迭代器与遍历方式

3.1 Iterator 迭代器

迭代器就像一辆"浏览车",带着你在集合里一个个浏览元素,看一个走一个,不能回头。

复制代码
Collection<String> c = new ArrayList<>();
c.add("苹果");
c.add("香蕉");
c.add("橘子");

Iterator<String> it = c.iterator();
while (it.hasNext()) {        // 前面还有吗?
    String fruit = it.next(); // 有,拿过来看看
    System.out.println(fruit);
}

核心方法:

方法 作用
hasNext() 判断是否还有下一个元素
next() 获取下一个元素
remove() 删除迭代器刚返回的元素

⚠️ 重要 :遍历集合时如果要删除元素,必须用迭代器的 remove() ,不能用集合的 remove(),否则会抛 ConcurrentModificationException

复制代码
// ❌ 错误:边遍历边用集合 remove
for (String fruit : c) {
    if (fruit.equals("香蕉")) {
        c.remove("香蕉"); // 抛 ConcurrentModificationException
    }
}

// ✅ 正确:用迭代器 remove
Iterator<String> it = c.iterator();
while (it.hasNext()) {
    String fruit = it.next();
    if (fruit.equals("香蕉")) {
        it.remove();
    }
}

3.2 增强 for 循环(foreach)

增强 for 本质就是迭代器的语法糖,写法更简单,但不能删除元素(因为你拿不到迭代器对象)。

复制代码
for (String fruit : c) {
    System.out.println(fruit);
}

3.3 Lambda forEach(Java 8+)

复制代码
// 完整写法
c.forEach((String fruit) -> {
    System.out.println(fruit);
});

// 最简写法(方法引用)
c.forEach(System.out::println);

3.4 四种遍历方式选择

场景 推荐用法
只想快速遍历一遍 增强 for
遍历时需要删除元素 迭代器
代码越少越好 Lambda / 方法引用
有索引、需要修改元素 普通 for(仅 List)

四、List 体系与源码

4.1 List 独有方法与五种遍历

List 比 Collection 多了索引功能,因此多了一批"按位置操作"的方法:

方法 作用
add(int index, E e) 在指定位置插入
get(int index) 获取指定位置元素
set(int index, E e) 修改指定位置元素
remove(int index) 删除指定位置元素
indexOf(Object o) 查找元素第一次出现的位置
subList(int from, int to) 截取子列表(包头不包尾)
复制代码
List<String> list = new ArrayList<>();
list.add("苹果");
list.add("香蕉");
list.add("橘子");

list.add(1, "西瓜");              // [苹果, 西瓜, 香蕉, 橘子]
System.out.println(list.get(0));  // 苹果
list.set(0, "草莓");              // [草莓, 西瓜, 香蕉, 橘子]
list.remove(1);                   // [草莓, 香蕉, 橘子]
System.out.println(list.indexOf("香蕉")); // 1
List<String> sub = list.subList(0, 2);   // [草莓, 香蕉]

List 的五种遍历:

遍历方式 有索引 能删除 能修改 能倒着走 适用场景
① 普通 for 需要索引、修改元素
② 增强 for 只是看看,最常用
③ Iterator 遍历时删元素
④ ListIterator 功能最全,双向遍历
⑤ Lambda 代码最简洁
复制代码
// ListIterator 是迭代器的升级版,支持双向遍历、添加、修改
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
    String fruit = it.next();
    System.out.println(fruit);
}
while (it.hasPrevious()) {
    String fruit = it.previous();
    System.out.println(fruit);
}

4.2 ArrayList 源码逐行解析

源码位置:JDK 安装目录 lib/src.zipjava/util/ArrayList.java,或 IDE 中按住 Ctrl 点击 ArrayList。

4.2.1 类声明与核心字段
复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组:用于 new ArrayList(0)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认空数组:用于 new ArrayList()
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正存数据的底层数组
    transient Object[] elementData;

    // 实际元素个数
    private int size;
}

几个关键理解:

  • size实际存了几个元素elementData.length数组总容量 ,永远 size <= elementData.length
  • 两个空数组 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 用于区分"指定容量为 0"和"没指定容量用默认",第一次 add 时后者会扩容到 10。

内存示意(new ArrayList<>() 后 add 了 3 个元素):

复制代码
elementData → ["A","B","C",null,null,null,null,null,null,null]
                0    1    2    3   ...           9
                ↑ size=3          ↑ 容量=10
4.2.2 构造方法
复制代码
// 无参构造:只把 elementData 指向空数组,此时容量是 0,不是 10
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 指定容量
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

// 传入一个集合
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
4.2.3 add(E e) 方法
复制代码
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // ① 检查容量
    elementData[size++] = e;           // ② 放数据,size+1
    return true;
}

链式调用:

复制代码
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 第一次 add:minCapacity = max(10, size+1) = 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;  // 修改次数,用于 fail-fast
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);  // 装不下就扩容
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 + 旧容量/2 = 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;  // 1.5 倍还不够就直接到够用
    elementData = Arrays.copyOf(elementData, newCapacity);
}

关键点oldCapacity >> 1 是右移一位 = 除以 2,所以扩容是 1.5 倍addAll 一次性加很多元素时,1.5 倍不够会直接扩到刚好够用。

4.2.4 add(int index, E element) 指定位置插入
复制代码
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);
    // 从 index 开始,把后面的元素统一往后挪一位
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
4.2.5 get / remove / addAll
复制代码
// get:直接按下标取,O(1)
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

// remove:保存旧值 → 把后面的往前挪 → size-1 → 末尾置 null 帮助 GC
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1,
                         elementData, index, numMoved);
    elementData[--size] = null;
    return oldValue;
}

// addAll:一次性扩容 + 一次性拷贝,比循环 add 快很多
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}
4.2.6 ArrayList 核心总结
  • 底层数组 elementData,默认容量 10,扩容 1.5 倍。
  • get(i) 直接数组寻址 O(1);中间插入/删除需要移动元素 O(n)。
  • addAll 一次性扩容到位,比循环 add 高效。
  • modCount 记录修改次数,用于迭代器 fail-fast 机制。

4.3 LinkedList 源码逐行解析

4.3.1 底层 Node 结构
复制代码
public class LinkedList<E> extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

    transient int size = 0;
    transient Node<E> first;  // 头节点
    transient Node<E> last;   // 尾节点

    private static class Node<E> {
        E item;
        Node<E> next;  // 后指针
        Node<E> prev;  // 前指针

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

双向链表结构:

复制代码
null ← [10] ⇄ [20] ⇄ [30] → null
        ↑                ↑
       first            last
4.3.2 添加元素
复制代码
// 尾部添加
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++;
    modCount++;
}

// 头部添加
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
4.3.3 删除元素
复制代码
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;
    }
    x.item = null;  // 帮助 GC
    size--;
    modCount++;
    return element;
}
4.3.4 getNode 的折半优化
复制代码
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;
    }
}

size >> 1 就是 size / 2。最坏仍是 O(n),但平均只走一半。

4.3.5 fail-fast 机制
复制代码
// 迭代器创建时记录期望的修改次数
int expectedModCount = modCount;

// 每次迭代检查
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
  • 迭代过程中如果通过集合本身add/remove 修改了集合,modCount 会变,导致 expectedModCount != modCount,立刻抛异常。
  • 迭代器自己的 remove() 会同步更新 expectedModCount,不会报错。
4.3.6 LinkedList vs ArrayList
对比项 LinkedList ArrayList
底层 双向链表 动态数组
查询 O(n) O(1)
头部增删 O(1) O(n)
中间增删 O(1)* O(n)
内存 大(每节点 3 个引用) 小(连续内存)
适用 频繁增删、栈/队列 频繁查询

五、常见数据结构基础

5.1 栈(Stack,LIFO)

后进先出,像一摞盘子:

复制代码
Deque<String> stack = new ArrayDeque<>();
stack.push("A");   // [A]
stack.push("B");   // [A, B]  B 压在 A 上
stack.push("C");   // [A, B, C]

stack.pop();       // 弹出 C,栈变 [A, B]
stack.pop();       // 弹出 B,栈变 [A]

5.2 队列(Queue,FIFO)

先进先出,像排队打饭:

复制代码
Queue<String> queue = new LinkedList<>();
queue.offer("A");  // [A]
queue.offer("B");  // [A, B]
queue.offer("C");  // [A, B, C]

queue.poll();      // 弹出 A,队列变 [B, C]
queue.poll();      // 弹出 B,队列变 [C]

5.3 数组 vs 链表

对比项 数组 链表
内存 连续 不连续,靠指针连接
随机访问 O(1) O(n)
中间插入/删除 O(n)(要移动元素) O(1)(已知位置)
空间开销 大(每个节点存指针)

5.4 二叉树 / BST / AVL

  • 二叉树:每个节点最多两个子节点。
  • 二叉查找树 BST:左子树 < 父节点 < 右子节点。
  • AVL 树:在 BST 基础上,任意节点左右子树高度差 ≤ 1,通过旋转保持平衡。

AVL 四种旋转:

类型 失衡位置 旋转方式
LL 左子树的左边插入 右单旋
RR 右子树的右边插入 左单旋
LR 左子树的右边插入 先左旋再右旋
RL 右子树的左边插入 先右旋再左旋

口诀:LL 右旋,RR 左旋,LR 先左后右,RL 先右后左。旋转不改变中序遍历顺序,只调整形状。

5.5 复杂度总览

数据结构 查找 插入 删除
O(n) O(1) 栈顶 O(1) 栈顶
队列 O(n) O(1) 队尾 O(1) 队头
数组 O(1) O(n) O(n)
链表 O(n) O(1) O(1)
BST O(log n)~O(n) O(log n)~O(n) O(log n)~O(n)
AVL O(log n) O(log n) O(log n)
红黑树 O(log n) O(log n) O(log n)

六、Set 体系

6.1 HashSet 与 LinkedHashSet 底层原理

HashSet 一句话:底层就是一个简化版 HashMap ,元素作为 key,value 统一用一个固定对象 PRESENT

复制代码
public class HashSet<E> {
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();

    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }
}

去重两步走:

  1. 先算 hashCode() 找到对应桶;
  2. 桶里已有元素再用 equals() 比较,相同就不存。

LinkedHashSet = HashSet + 双向链表:在 HashMap 基础上额外维护一条记录插入顺序的双向链表,遍历从 head 走到 tail,所以能保持插入顺序。

对比 HashSet LinkedHashSet
顺序 ❌ 无序 ✅ 插入顺序
性能 最快 O(1) 稍慢(多维护链表)
额外开销 每元素多两个指针

6.2 TreeSet 排序(Comparable 与 Comparator)

TreeSet 底层是红黑树,保证元素唯一且自动排序。有两种排序方式:

方式一:自然排序(Comparable)

让自定义类实现 Comparable 接口,重写 compareTo()

复制代码
public class Student implements Comparable<Student> {
    private String name;
    private int score;
    private int age;

    public Student(String name, int score, int age) {
        this.name = name;
        this.score = score;
        this.age = age;
    }

    @Override
    public int compareTo(Student other) {
        // 先按成绩降序
        int result = other.score - this.score;
        // 成绩相同按年龄升序
        if (result == 0) {
            result = this.age - other.age;
        }
        return result;
    }
}

compareTo 返回值规则(小负等大零,大正往后排):

返回值 含义
负数 当前对象 < 参数对象,排在前面
0 认为重复,不添加
正数 当前对象 > 参数对象,排在后面
方式二:定制排序(Comparator)

不修改原类,创建 TreeSet 时传入比较器,优先级高于自然排序

复制代码
// Lambda 写法(推荐)
TreeSet<Student> ts = new TreeSet<>((s1, s2) -> {
    int result = s1.getScore() - s2.getScore();
    if (result == 0) {
        return s1.getAge() - s2.getAge();
    }
    return result;
});

也可以用 Comparator 链式写法:

复制代码
Comparator<Student> comparator = Comparator
        .comparingInt(Student::getScore)
        .thenComparingInt(Student::getAge);
TreeSet<Student> ts = new TreeSet<>(comparator);
Comparable vs Comparator
特性 Comparable Comparator
方法 compareTo() compare()
位置 类内部实现 创建集合时传入
侵入性 高(要改原类) 低(不改原类)
排序规则 只能一种 多种,灵活
优先级 高(覆盖 Comparable)

⚠️ compareTo/compare 返回 0 会被认为是重复元素,不会被添加。多条件比较时要小心,不要轻易返回 0。


七、泛型与通配符 PECS

7.1 为什么要有泛型

没有泛型时,集合能放任何类型,取出来要强转,容易 ClassCastException;有了泛型,编译期就能检查类型,还能消除强转。

复制代码
// 泛型类
public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// 泛型方法(注意:静态方法必须自己声明泛型,不能用类的 T)
public static <T> void print(T value) {
    System.out.println(value);
}

7.2 泛型擦除

Java 泛型是编译期的,运行时泛型信息被擦除:

复制代码
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true

所以不能 new T()、不能 new T[10]、不能 instanceof List<String>(只能 instanceof List)。

7.3 三种通配符

通配符 含义 能读 能写
? 任意类型 读成 Object 只能写 null
? extends T T 或 T 的子类(上界) 读成 T 只能写 null
? super T T 或 T 的父类(下界) 读成 Object 能写 T 及其子类

PECS 原则:Producer Extends, Consumer Super

  • 集合是生产者 (只读)→ 用 ? extends T
  • 集合是消费者 (只写)→ 用 ? super T

7.4 实战示例:Animal / Cat / Dog 继承结构

复制代码
class Animal { String name; int age; }
class Cat extends Animal {}
class Dog extends Animal {}
class PersianCat extends Cat {}
class Teddy extends Dog {}

// 要求1:只能养所有猫,不能养狗
public static void keepPet1(ArrayList<? extends Cat> list) {
    for (Cat cat : list) { cat.eat(); }
}

// 要求2:只能养所有狗,不能养猫
public static void keepPet2(ArrayList<? extends Dog> list) {
    for (Dog dog : dog) { dog.eat(); }
}

// 要求3:养所有动物
public static void keepPet3(ArrayList<? extends Animal> list) {
    for (Animal animal : list) { animal.eat(); }
}

Collections.copy(dest, src) 就是典型的 PECS 应用:src 是生产者用 ? extendsdest 是消费者用 ? super


八、Map 体系

8.1 Map 接口与常用 API

Map 是双列集合,一个 key 对应一个 value。核心特点:

  • Key 不重复,Value 可以重复;
  • Key 重复时新 value 覆盖旧 value;
  • HashMap 允许 null key/value,TreeMap 不允许;
  • HashMap 无序,LinkedHashMap 保插入顺序,TreeMap 按 key 排序。
API 作用 示例
put(K, V) 添加/更新 map.put("张三", 23)
get(K) 获取值 map.get("张三")
getOrDefault(k, 默认值) 安全获取 map.getOrDefault("李四", 0)
remove(K) 删除 map.remove("张三")
containsKey(K) 判断 key 存在 map.containsKey("张三")
containsValue(V) 判断 value 存在 map.containsValue(23)
size() / isEmpty() 个数 / 是否空
keySet() 所有 key
entrySet() 所有键值对(最高效)
forEach((k,v) -> ...) Lambda 遍历

Map 三种遍历:

复制代码
// ① keySet:先拿 key 再 get,效率较低
for (String key : map.keySet()) {
    System.out.println(key + " -> " + map.get(key));
}

// ② entrySet:直接拿键值对,推荐
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

// ③ forEach + Lambda(最简洁)
map.forEach((k, v) -> System.out.println(k + " -> " + v));

8.2 HashMap 基本使用

复制代码
HashMap<String, String> map = new HashMap<>();
map.put("name", "张三");
map.put("age", "20");
map.put("city", "厦门");

System.out.println(map.get("name"));             // 张三
System.out.println(map.getOrDefault("phone", "未知")); // 未知
map.remove("age");
System.out.println(map.containsKey("name"));     // true

核心特性速记:

  • 存储键值对,Key 不可重复,Value 可重复;
  • 允许 null key / null value;
  • 无序,查询平均 O(1);
  • 线程不安全,多线程用 ConcurrentHashMap

8.3 HashMap 源码深度解析

8.3.1 核心常量
复制代码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 默认加载因子

static final int TREEIFY_THRESHOLD = 8;              // 链表长度 >= 8 树化
static final int UNTREEIFY_THRESHOLD = 6;           // 红黑树节点 <= 6 退化
static final int MIN_TREEIFY_CAPACITY = 64;         // 容量 < 64 优先扩容而非树化

transient Node<K,V>[] table;  // 哈希桶数组
transient int size;
int threshold;                 // 扩容阈值 = 容量 * 加载因子
final float loadFactor;
8.3.2 扰动函数 hash()
复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

把 hashCode 的高 16 位和低 16 位异或,让高位也参与索引计算,减少哈希冲突。

8.3.3 索引计算
复制代码
index = (n - 1) & hash;

为什么容量必须是 2 的幂? 因为 n - 1 的二进制低位全是 1(如 16-1=15=01111),& hash 后低位充分参与运算,分布均匀。

8.3.4 put 流程(putVal)
复制代码
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. 懒初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 桶为空,直接放
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 3.1 第一个 key 相同,覆盖
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 3.2 已树化,走红黑树插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>) p).putTreeVal(this, tab, hash, key, value);
        // 3.3 链表尾插
        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, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 4. key 已存在,覆盖旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            return oldValue;
        }
    }
    ++modCount;
    // 5. 超过阈值就扩容
    if (++size > threshold)
        resize();
    return null;
}
8.3.5 扩容 resize() 与 (hash & oldCap) 优化

扩容时容量翻倍,JDK 1.8 做了一个重要优化:不需要重新计算 hash ,只用 (hash & oldCap) 判断节点留在原位置还是搬到 原位置 + oldCap

复制代码
if ((next.hash & oldCap) == 0) {
    // 低位组:索引不变
    loTail.next = next;
} else {
    // 高位组:新索引 = 原索引 + oldCap
    hiTail.next = next;
}

扩容触发时机:

  • 默认容量 16,加载因子 0.75,阈值 12;
  • 第 13 个元素插入时触发扩容,容量翻倍到 32。
8.3.6 为什么树化阈值是 8?

基于泊松分布:加载因子 0.75 时,链表长度达到 8 的概率约为 0.00000006,极低。正常情况下几乎不会树化;只有哈希分布异常时才树化,把 O(n) 降到 O(log n)。同时容量 < 64 时优先扩容而非树化。

8.3.7 HashMap 线程不安全
  • JDK 1.7 并发扩容可能导致链表成环,get 死循环;
  • JDK 1.8 并发 put 可能数据覆盖;
  • 解决方案:用 ConcurrentHashMap

8.4 LinkedHashMap 与 LRU

LinkedHashMap 继承 HashMap,额外维护一条双向链表记录插入顺序(或访问顺序)。

复制代码
// 默认按插入顺序
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 遍历:A -> B -> C

// 按访问顺序(构造参数 accessOrder = true)
LinkedHashMap<String, Integer> map2 =
        new LinkedHashMap<>(16, 0.75f, true);
map2.put("A", 1);
map2.put("B", 2);
map2.put("C", 3);
map2.get("B");  // 访问 B,B 被移到尾部
// 遍历:A -> C -> B

经典 LRU 缓存实现:

复制代码
int capacity = 100;
LinkedHashMap<String, Object> cache =
        new LinkedHashMap<>(16, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
        return size() > capacity;  // 超过容量,淘汰最久未使用的
    }
};

8.5 TreeMap 与范围查询

TreeMap 底层是红黑树 ,按 key 自动排序,put/get/remove 都是 O(log n),key 不能为 null

复制代码
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Apple", 10);
map.put("Banana", 20);
map.put("Cherry", 30);
// 输出按 key 字典序:Apple -> Banana -> Cherry

范围查询常用方法:

方法 作用
firstKey() / lastKey() 最小 / 最大 key
higherKey(k) / lowerKey(k) 严格大于 / 小于 k 的 key
ceilingKey(k) / floorKey(k) 大于等于 / 小于等于 k 的 key
headMap(toKey) 小于 toKey 的子 Map
tailMap(fromKey) 大于等于 fromKey 的子 Map
subMap(from, to) 区间子 Map
复制代码
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "A"); map.put(3, "B");
map.put(5, "C"); map.put(7, "D");

map.subMap(3, true, 7, true); // {3=B, 5=C, 7=D}
map.headMap(5);               // {1=A, 3=B}
map.tailMap(5);               // {5=C, 7=D}

三种 Map 对比:

特性 HashMap LinkedHashMap TreeMap
排序 无序 插入顺序 key 排序
复杂度 O(1) O(1) O(log n)
null key 允许 允许 不允许
适用 通用 保序 / LRU 排序 / 范围查询

九、红黑树原理

9.1 五条铁律(必背)

  1. 每个节点要么红,要么黑;
  2. 根节点是黑色
  3. 叶子节点(null)是黑色;
  4. 红色节点的子节点必须是黑色(不能红红相连);
  5. 从任意节点到其叶子的每条路径,黑色节点数量相同(黑高相同)。

9.2 为什么能保证平衡

  • 最短路径:全黑;
  • 最长路径:红黑交替;
  • 由规则 4、5 推出:最长路径不超过最短路径的 2 倍,所以高度仍是 O(log n)。

9.3 插入修复

新节点默认是红色(插黑色会立刻破坏规则 5,代价更大),然后分三种情况:

情况 处理
新节点是根 直接染黑
父节点是黑色 不用动
父节点红色(红红冲突) 看叔叔节点:
------ 叔叔是红色 父、叔变黑,祖父变红,继续向上检查
------ 叔叔黑 + LL/RR 变色 + 单旋
------ 叔叔黑 + LR/RL 先旋成直线,再变色 + 单旋

9.4 红黑树 vs AVL

对比 AVL 红黑树
平衡条件 严格(BF ≤ 1) 宽松(最长路径 ≤ 最短 ×2)
查找 更快 稍慢
插入删除 旋转频繁 旋转较少
适用 查找为主 增删频繁(Java 的 TreeMap/HashMap 树化都用它)

十、JDK5 可变参数

10.1 语法与本质

复制代码
public static int add(int... numbers) {
    int sum = 0;
    for (int n : numbers) sum += n;
    return sum;
}
  • 类型... 参数名 声明;

  • 编译后本质就是数组

  • 一个方法只能有一个 可变参数,且必须放在最后

    System.out.println(add()); // 0(空数组,不是 null)
    System.out.println(add(1)); // 1
    System.out.println(add(1, 2, 3)); // 6

10.2 重载优先级

精确匹配 > 宽化 > 装箱 > 可变参数。也就是说,show(int x) 会优先于 show(int... nums) 被调用。


十一、Collections 工具类

注意:Collection 是接口,Collections工具类(全静态方法)。

11.1 常用方法

分类 方法 作用
排序 sort(list) 升序排序
sort(list, comparator) 自定义排序
reverse(list) 反转
shuffle(list) 随机打乱
查找 max(list) / min(list) 最大 / 最小值
binarySearch(list, key) 二分查找(必须先排序)
frequency(list, obj) 统计出现次数
替换 replaceAll(list, old, new) 全部替换
填充 fill(list, val) 全部填充
copy(dest, src) 拷贝
nCopies(n, obj) 生成 n 个相同元素的不可变列表
不可变 unmodifiableList/list/map 只读视图
同步 synchronizedList/Set/Map 包装成线程安全
复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(5, 3, 8, 1, 9));

Collections.sort(list);                    // [1, 3, 5, 8, 9]
Collections.sort(list, Collections.reverseOrder()); // [9, 8, 5, 3, 1]
Collections.shuffle(list);                // 随机打乱
Collections.max(list);                    // 9
Collections.frequency(list, 3);           // 3 出现次数
Collections.binarySearch(list, 7);        // 二分查找,需先排序

不可变视图:

复制代码
List<String> readOnly = Collections.unmodifiableList(list);
readOnly.add("X");  // 抛 UnsupportedOperationException

线程安全:

复制代码
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 遍历时仍需手动加锁
synchronized (syncList) {
    for (String s : syncList) { System.out.println(s); }
}

十二、面试高频问题汇总

Q1:ArrayList 和 LinkedList 区别?

  • ArrayList 基于数组,查询 O(1)、增删 O(n);
  • LinkedList 基于双向链表,查询 O(n)、增删 O(1)(已知节点);
  • 绝大多数场景用 ArrayList。

Q2:ArrayList 扩容机制?

  • 默认容量 10,扩容为原来的 1.5 倍(oldCap + oldCap >> 1),通过 Arrays.copyOf 复制。

Q3:HashMap 底层原理?

  • JDK 1.8:数组 + 链表 + 红黑树;
  • 链表长度 ≥ 8 且数组容量 ≥ 64 时树化,≤ 6 时退化;
  • 哈希冲突用链地址法解决。

Q4:HashMap 扩容机制?

  • 默认容量 16、加载因子 0.75、阈值 12;
  • 元素超过阈值翻倍扩容;JDK 1.8 用 (hash & oldCap) 优化迁移,不用重算 hash。

Q5:为什么 HashMap 容量是 2 的幂?

  • 为了 (n - 1) & hash 均匀分布,让 hash 低位充分参与运算。

Q6:HashMap 和 Hashtable / ConcurrentHashMap 区别?

  • HashMap 线程不安全、允许 null;
  • Hashtable 线程安全(全表锁)、不允许 null;
  • ConcurrentHashMap 线程安全(分段锁/CAS)、性能高。

Q7:HashSet 怎么去重?

  • 底层就是 HashMap,元素作为 key,value 是固定的 PRESENT 对象;先比 hashCode,再比 equals。

Q8:Comparable 和 Comparator 区别?

  • Comparable 在类内部实现 compareTo(),只能一种排序;
  • Comparator 在创建集合时传入 compare(),多种排序,优先级更高。

Q9:什么是 fail-fast?

  • 迭代过程中检测到集合被非迭代器方式修改,立刻抛 ConcurrentModificationException
  • 用迭代器自己的 remove()、并发集合或加锁可避免。

Q10:红黑树五条规则?

  • 节点非红即黑;根黑;叶子黑;红红不相连;任意路径黑高相同。

Q11:? extends T? super T 区别?(PECS)

  • ? extends T:生产者,只能读成 T,不能写;
  • ? super T:消费者,能写 T 及其子类,读出来是 Object。

Q12:LinkedHashMap 怎么实现 LRU?

  • 构造时 accessOrder = true,每次 get 把节点移到链表尾部;重写 removeEldestEntry,超过容量删除头部最久未使用节点。

📝 笔记整理说明:本文基于 2026 年 5 月学习笔记整理,覆盖 Java 集合框架从入门到源码的核心知识点。建议配合 IDE 查看 JDK 源码,效果更佳。如果觉得有帮助,欢迎点赞收藏~

相关推荐
wno7042 小时前
Spring Security短信验证码登录
java·python·spring
泡泡鱼(敲代码中)2 小时前
Python 容器类型学习笔记:列表、元组、字典、集合
开发语言·笔记·python·pycharm
步行cgn3 小时前
Spring util 命名空间详解
java·后端·spring
2501_933670793 小时前
平台招商运营校招能力模型:数据分析、SQL与增长场景拆解
开发语言
古法安卓3 小时前
Android-休眠唤醒后onLocationChanged没有数据问题排查
android·java·android studio
怕浪猫3 小时前
从 Windows 换到 Mac 三个月,我真香了
算法·面试·架构
菜鸟~noob2333 小时前
【Bonjour】华为Peerium架构深度解析:从冯·诺依曼到百万处理器“一台计算机”,附MATLAB性能仿真【含matlab代码,可直接运行】
开发语言·matlab·华为·peerium
周GZ4 小时前
简单讲解线程池的4种拒绝策略
java
白远山4 小时前
24小时自助健身房系统开发实战:从需求分析到完整指南
java·开发语言·数据库·数据挖掘·需求分析
暮雨哀尘4 小时前
JAVA基础练习(四):SpringBoot 实现简易登录功能
java·spring boot·eclipse·maven