本文整理自学习笔记,覆盖 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 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.zip→java/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_ELEMENTDATA和DEFAULTCAPACITY_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;
}
}
去重两步走:
- 先算
hashCode()找到对应桶; - 桶里已有元素再用
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是生产者用? extends,dest是消费者用? 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 五条铁律(必背)
- 每个节点要么红,要么黑;
- 根节点是黑色;
- 叶子节点(null)是黑色;
- 红色节点的子节点必须是黑色(不能红红相连);
- 从任意节点到其叶子的每条路径,黑色节点数量相同(黑高相同)。
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 源码,效果更佳。如果觉得有帮助,欢迎点赞收藏~