Java 集合讲解
第一模块:集合基础 —— 搞懂 “顶层规范” 与 “遍历逻辑”
1. 集合进阶 - 01:单列集合顶层接口 Collection
首先要明确:集合是 “存储多个数据的容器”,但它和数组的本质区别是 “动态扩容”(数组长度固定,集合长度可自动调整)。而 Collection
就是所有 单列集合(只存单个元素,如 List/Set) 的 “顶层接口”—— 它定义了所有单列集合的通用行为,相当于 “制定了一套规则”。
① Collection 接口的核心方法(必须掌握)
这些方法是所有单列集合(ArrayList、HashSet 等)都能直接用的,我们用代码案例演示:
import java.util.ArrayList;
import java.util.Collection;
public class CollectionDemo {
public static void main(String[] args) {
// 1. 创建Collection实现类对象(接口不能直接new,用ArrayList实例化)
Collection<String> coll = new ArrayList<>();
// 2. 核心方法使用
coll.add("张三"); // 添加元素,返回true(添加成功)
coll.add("李四");
coll.add("王五");
System.out.println(coll); // 输出:[张三, 李四, 王五]
System.out.println(coll.size()); // 获取元素个数,输出:3
System.out.println(coll.contains("李四")); // 判断是否包含某个元素,输出:true
coll.remove("王五"); // 删除元素,返回true(删除成功)
System.out.println(coll); // 输出:[张三, 李四]
coll.clear(); // 清空所有元素
System.out.println(coll.isEmpty()); // 判断是否为空,输出:true
}
}
② 关键注意点
Collection
是接口,不能直接new Collection()
,必须用它的实现类(如 ArrayList、HashSet)实例化;- 方法的返回值要重视:
add()
/remove()
返回布尔值,表示 “操作是否成功”(比如向 Set 中添加重复元素,add()
会返回 false); - 泛型
<String>
:指定集合中只能存 String 类型(Java 5 + 特性,避免类型转换异常,后面会详细讲泛型)。
2. 集合进阶 - 02:迭代器(Iterator)—— 集合的 “专属遍历工具”
当我们需要 “逐个访问集合中的元素” 时,就需要 迭代器(Iterator)—— 它是 Collection 接口的 “内部工具”,所有单列集合都能通过 iterator()
方法获取迭代器对象。
① 迭代器的核心方法
迭代器只有 3 个核心方法,逻辑是 “先判断、再获取、后删除”:
方法 | 作用 |
---|---|
hasNext() | 判断 “下一个位置是否有元素”,返回布尔值 |
next() | 获取 “下一个位置的元素”,并移动指针 |
remove() | 删除 “上一个被 next () 获取的元素” |
② 代码案例:用迭代器遍历 Collection
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("苹果");
coll.add("香蕉");
coll.add("橙子");
// 1. 获取迭代器对象
Iterator<String> it = coll.iterator();
// 2. 遍历:hasNext()判断,next()获取
while (it.hasNext()) { // 先判断“有没有下一个”
String fruit = it.next(); // 再获取“下一个元素”,指针后移
System.out.println(fruit); // 输出:苹果、香蕉、橙子
// 3. (可选)删除元素:只能删“上一个next()获取的元素”
if ("香蕉".equals(fruit)) {
it.remove(); // 注意:不能用coll.remove()!会触发ConcurrentModificationException
}
}
System.out.println(coll); // 输出:[苹果, 橙子]
}
}
③ 避坑重点:ConcurrentModificationException(并发修改异常)
为什么不能在迭代器遍历的时候,用集合的 remove()
方法删元素?
- 迭代器有一个 “修改计数器”:当迭代器创建后,若集合通过自身方法(如
coll.remove()
)修改了元素个数,计数器会变化; - 迭代器每次
next()
时都会检查计数器:如果发现 “迭代器记录的计数器” 和 “集合当前的计数器” 不一致,就会抛出异常。 - 正确做法:用迭代器自身的
it.remove()
方法,它会同步更新计数器,避免异常。
3. 集合进阶 - 03:增强 for 循环 & Lambda 表达式 —— 更简洁的遍历
迭代器的语法有点繁琐,Java 提供了更简单的遍历方式:增强 for 循环(foreach) 和 Lambda 表达式(Java 8+),它们本质上还是 “迭代器的语法糖”。
① 增强 for 循环(foreach)
语法:for (元素类型 变量名 : 集合/数组) { 循环体 }
特点:简洁,适合 “只遍历、不修改” 的场景(修改元素会有问题,比如遍历 List 时改元素值可以,但删元素仍会抛异常)。
代码案例:
Collection<String> coll = new ArrayList<>();
coll.add("Java");
coll.add("Python");
coll.add("Go");
// 增强for遍历集合
for (String lang : coll) {
System.out.println(lang); // 输出:Java、Python、Go
}
// 增强for也能遍历数组(本质和集合无关,只是语法统一)
String[] arr = {"张三", "李四"};
for (String name : arr) {
System.out.println(name); // 输出:张三、李四
}
② Lambda 表达式(Java 8+)
Collection 接口在 Java 8 中新增了 forEach()
方法,接收一个 “消费型接口(Consumer)” 参数,配合 Lambda 表达式可以实现 “一行遍历”。
语法:集合.forEach(元素 -> 遍历操作)
代码案例:
Collection<String> coll = new ArrayList<>();
coll.add("北京");
coll.add("上海");
coll.add("广州");
// Lambda表达式遍历
coll.forEach(city -> System.out.println(city)); // 输出:北京、上海、广州
// 简化版(方法引用):当“遍历操作是调用一个已有的静态方法/实例方法”时
coll.forEach(System.out::println); // 效果和上面一样,更简洁
③ 三种遍历方式对比
遍历方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
迭代器(Iterator) | 支持遍历中删除元素 | 语法繁琐 | 需要删除元素的场景 |
增强 for(foreach) | 语法简洁 | 不支持删除元素,不能获取索引(List) | 仅遍历、不修改的场景 |
Lambda 表达式 | 最简洁(一行代码) | 不支持删除元素,可读性依赖 Lambda 熟练度 | Java 8+,简单遍历场景 |
4. 集合进阶 - 04:List 中常见的方法和五种遍历方式
List
是 Collection 的子接口,它的核心特点是 “有序(元素存入顺序 = 取出顺序)、可重复(允许存多个相同元素)、支持索引(像数组一样通过下标访问)”—— 这也是它和 Set 的核心区别。
① List 的专属方法(比 Collection 多的方法)
因为支持索引,List 新增了一批 “基于索引操作” 的方法,核心如下:
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a"); // 末尾添加元素(Collection的方法)
list.add(1, "b"); // 专属:在索引1的位置插入元素
System.out.println(list); // 输出:[a, b]
list.set(1, "c"); // 专属:修改索引1的元素值
System.out.println(list); // 输出:[a, c]
String elem = list.get(0); // 专属:获取索引0的元素
System.out.println(elem); // 输出:a
int index = list.indexOf("c"); // 专属:查找元素第一次出现的索引
System.out.println(index); // 输出:1
list.remove(0); // 专属:删除索引0的元素(注意和Collection的remove(元素)区分)
System.out.println(list); // 输出:[c]
}
}
② List 的五种遍历方式(重点!)
因为 List 支持索引,比 Collection 多了 “普通 for 循环” 和 “ListIterator 双向遍历” 两种方式,共 5 种:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class ListTraverse {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
// 1. 普通for循环(List专属,利用索引)
System.out.println("=== 普通for ===");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 2. 增强for循环
System.out.println("=== 增强for ===");
for (String name : list) {
System.out.println(name);
}
// 3. 迭代器(Iterator)
System.out.println("=== Iterator ===");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 4. ListIterator(List专属,双向遍历)
System.out.println("=== ListIterator(双向) ===");
ListIterator<String> lit = list.listIterator(list.size()); // 从末尾开始
while (lit.hasPrevious()) { // 反向判断:“上一个位置有没有元素”
String name = lit.previous(); // 反向获取:“上一个元素”,指针前移
System.out.println(name); // 输出:王五、李四、张三(反向遍历)
}
// 5. Lambda表达式
System.out.println("=== Lambda ===");
list.forEach(System.out::println);
}
}
第二模块:数据结构基础 —— 理解 “集合底层的本质”
学完 List 的用法,你可能会问:为什么 ArrayList 查询快、增删慢?为什么 LinkedList 增删快、查询慢? 答案在 “数据结构” 里 —— 集合的性能完全由底层数据结构决定。
集合进阶 - 05:核心数据结构(栈、队列、数组、链表)
我们先讲 4 种最基础的数据结构,它们是 ArrayList、LinkedList 的底层实现:
1. 数组(ArrayList 底层)
- 结构特点:连续的内存空间,每个元素有唯一的 “索引(下标)”,像一排整齐的柜子,每个柜子有编号。
核心性能:
- 查询快:通过索引直接定位元素,时间复杂度 O(1)(不管数组多大,找索引 5 的元素只要 1 步);
增删慢:
- 末尾增删:快(直接在最后加 / 删,O (1));
- 中间增删:慢(要移动后面所有元素,比如在索引 1 插入元素,后面的元素都要后移,数组越大越慢,O (n))。
- 缺点:长度固定(ArrayList 的 “动态扩容” 本质是 “创建新数组、复制旧元素”,不是真的在原数组上扩容)。
2. 链表(LinkedList 底层)
结构特点:不连续的内存空间,每个元素(节点)包含 “数据” 和 “指针(指向前后节点)”,像一串糖葫芦,每个糖葫芦通过绳子连起来。
- 单向链表:每个节点只有 “下一个节点的指针”;
- 双向链表(LinkedList 用的是这个):每个节点有 “上一个节点的指针” 和 “下一个节点的指针”。
核心性能:
- 查询慢:没有索引,要找某个元素必须从 “头节点” 开始遍历,数组越大越慢,时间复杂度 O(n);
增删快:
- 首尾增删:快(只要改首尾节点的指针,O (1));
- 中间增删:只要找到目标节点,改前后节点的指针即可(不需要移动其他元素,O (1))—— 前提是 “已经找到目标节点”(找节点的过程还是 O (n))。
- 优点:长度动态(不需要扩容,新增节点直接连在链表上)。
3. 栈(Stack)——“先进后出”(FILO)
- 结构特点:像一个杯子,只能从 “顶端” 添加和取出元素(类比:叠盘子,只能从最上面放 / 拿)。
核心操作:
push()
:向栈顶添加元素(压栈);pop()
:从栈顶删除并返回元素(弹栈);peek()
:查看栈顶元素(不删除)。
- 底层实现:LinkedList 可以实现栈(用
addFirst()
模拟 push,removeFirst()
模拟 pop)。
4. 队列(Queue)——“先进先出”(FIFO)
- 结构特点:像一条排队的队伍,只能从 “队尾” 添加元素,从 “队头” 取出元素(类比:买奶茶排队,先到先得)。
核心操作:
offer()
:向队尾添加元素(入队);poll()
:从队头删除并返回元素(出队);peek()
:查看队头元素(不删除)。
- 底层实现:LinkedList 可以实现队列(用
addLast()
模拟 offer,removeFirst()
模拟 poll)。
总结与下节预告
到这里,我们已经掌握了:
- 集合的顶层接口 Collection 的核心方法;
- 三种遍历方式(迭代器、增强 for、Lambda)的用法与避坑点;
- List 的专属方法与五种遍历方式;
- 数组、链表、栈、队列的结构与性能差异(这是理解 ArrayList 和 LinkedList 的关键)。
下一节,我们会深入 ArrayList 源码分析—— 看看它的 “动态扩容” 是怎么实现的?初始容量为什么是 10?以及 “数组结构” 在源码中是如何体现的,彻底搞懂 “ArrayList 为什么查询快”。
上一节我们深入了 ArrayList、LinkedList 的源码和迭代器原理,还掌握了泛型类、方法与接口的基础用法。这一节将完成泛型进阶(通配符),拆解红黑树这一核心数据结构,并讲解HashSet
、LinkedHashSet
的实现逻辑,建立 “数据结构→集合实现→使用场景” 的完整链路。
第四模块:泛型进阶 —— 通配符与综合应用
1. 集合进阶 - 10:泛型的通配符(?
)
泛型的核心是 “类型安全”,但当需要 “兼容多种泛型类型” 时(比如接收任意泛型的 List),直接写List<Object>
会有问题 —— 因为泛型不具备协变性(List<String>
不是List<Object>
的子类)。此时需要用泛型通配符?
解决。
① 通配符的基本用法:List<?>
(任意类型的 List)
List<?>
表示 “未知类型的 List”,可以接收任何泛型类型的 List,但有两个限制:
- 不能向集合中添加元素(除了
null
):因为不知道具体类型,添加任何类型都可能不安全; - 可以从集合中获取元素:获取的元素类型会被自动转为
Object
。
代码案例:
import java.util.ArrayList;
import java.util.List;
public class WildcardBasic {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
strList.add("Java");
List<Integer> intList = new ArrayList<>();
intList.add(2024);
// 调用方法:接收任意泛型的List
printList(strList); // 输出:[Java]
printList(intList); // 输出:[2024]
}
// 泛型通配符:List<?> 接收任意类型的List
public static void printList(List<?> list) {
// 允许获取元素(转为Object)
for (Object obj : list) {
System.out.println(obj);
}
// 不允许添加元素(除了null):编译报错
// list.add("hello"); // 错误:无法确定list的具体类型,添加String可能不安全
list.add(null); // 允许:null是所有类型的默认值
}
}
② 带上限的通配符:List<? extends E>
(E 及其子类)
当需要 “限制泛型类型为某个类的子类” 时,用? extends E
(比如 “只接收 Number 或其子类的 List”)。
核心特点:
- 可以获取元素:获取的元素类型为
E
(父类类型,安全); - 仍不能添加元素:因为无法确定具体是哪个子类(比如
List<? extends Number>
可能是List<Integer>
或List<Double>
,添加Integer
到Double
的 List 会出错)。
代码案例:
public class WildcardUpperBound {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(10);
List<Double> doubleList = new ArrayList<>();
doubleList.add(3.14);
// 允许接收Number的子类(Integer、Double)
calculateSum(intList); // 输出:10.0
calculateSum(doubleList); // 输出:3.14
// 不允许接收非Number子类(如String):编译报错
// List<String> strList = new ArrayList<>();
// calculateSum(strList); // 错误:String不是Number的子类
}
// 带上限的通配符:? extends Number(只接收Number及其子类)
public static double calculateSum(List<? extends Number> list) {
double sum = 0.0;
// 允许获取元素(转为Number类型,调用Number的方法)
for (Number num : list) {
sum += num.doubleValue(); // 所有Number子类都有doubleValue()方法
}
return sum;
}
}
③ 带下限的通配符:List<? super E>
(E 及其父类)
当需要 “限制泛型类型为某个类的父类” 时,用? super E
(比如 “只接收 Integer 或其父类的 List”)。
核心特点:
- 可以添加元素:添加
E
类型或其子类的元素(安全,因为父类 List 可以接收子类对象); - 获取元素时只能转为
Object
:因为无法确定具体是哪个父类。
代码案例:
import java.util.ArrayList;
import java.util.List;
public class WildcardLowerBound {
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
// 允许接收Integer的父类(Number、Object)
addInteger(numList); // numList变为:[100, 200]
addInteger(objList); // objList变为:[100, 200]
// 不允许接收Integer的子类(如Integer的子类不存在,或自定义子类):编译报错
// List<Integer> intList = new ArrayList<>();
// addInteger(intList); // 错误:Integer不是Integer的父类(下限是E,需≥E)
}
// 带下限的通配符:? super Integer(只接收Integer及其父类)
public static void addInteger(List<? super Integer> list) {
// 允许添加Integer及其子类(这里Integer没有子类,直接加Integer)
list.add(100);
list.add(200);
// 获取元素时只能转为Object
for (Object obj : list) {
System.out.println(obj);
}
}
}
④ 通配符使用场景总结
通配符类型 | 语法 | 能添加元素? | 能获取元素? | 适用场景 |
---|---|---|---|---|
任意类型 | List<?> | 只能加null | 能,转为Object | 仅遍历集合,不修改 |
上限(E 及其子类) | List<? extends E> | 不能(除null ) | 能,转为E | 读取集合元素(如计算总和) |
下限(E 及其父类) | List<? super E> | 能,加 E 或其子类 | 能,转为Object | 修改集合元素(如添加特定类型) |
第五模块:高级数据结构 —— 红黑树(TreeSet/TreeMap 底层)
在学习 Set 集合前,必须先理解红黑树—— 因为TreeSet
和TreeMap
的底层就是红黑树,它决定了这两个集合 “有序、无重复” 的特性。
1. 集合进阶 - 11~13:红黑树的核心原理
红黑树是一种自平衡的二叉查找树,它通过 5 条规则保证 “树的高度始终接近 log₂n”,从而让查询、插入、删除的时间复杂度稳定在 O (logn)(避免二叉查找树退化为链表的 O (n) 问题)。
① 红黑树的 5 条核心规则(必须牢记)
- 节点要么是红色,要么是黑色;
- 根节点必须是黑色;
- 所有叶子节点(NIL 节点,空节点)必须是黑色;
- 如果一个节点是红色,那么它的两个子节点必须是黑色(即 “红色节点不连续”,避免出现父 - 子 - 孙都是红色的情况);
- 从任意节点到其所有叶子节点的路径上,黑色节点的数量相同(称为 “黑高相等”,保证树的平衡)。
示意图(简化:省略 NIL 叶子节点):
10(黑) // 根节点黑
/ \
5(红) 15(红) // 红节点的子节点必须黑
/ \ / \
3(黑)7(黑)12(黑)20(黑) // 所有路径黑高相同(2个黑节点:10→5→3 或 10→15→20)
② 红黑树的 “自平衡” 机制:插入 / 删除后的调整
当插入或删除节点后,红黑树的规则可能被破坏(比如出现连续红节点、黑高不相等),此时需要通过两种操作恢复平衡:
旋转:分为左旋和右旋,本质是 “调整节点的父子关系”,不改变二叉查找树的有序性(左子树 < 根 < 右子树)。
- 左旋:将节点的右子树提升为父节点,原父节点降为左子树;
- 右旋:将节点的左子树提升为父节点,原父节点降为右子树。
- 变色:将节点的颜色从红改为黑,或从黑改为红(配合旋转使用,恢复红黑规则)。
③ 红黑树的核心优势
- 平衡性能好:即使频繁插入 / 删除,树的高度仍接近 log₂n,避免退化;
- 效率稳定:查询、插入、删除的时间复杂度均为 O (logn),比普通二叉查找树更可靠;
- 适合有序场景:因为是二叉查找树,中序遍历(左→根→右)可得到 “从小到大” 的有序序列(这是 TreeSet/TreeMap “有序” 的根源)。
第六模块:Set 集合 ——HashSet 与 LinkedHashSet
Set 集合的核心特性是 “无序、无重复”(TreeSet 是有序的特例),底层依赖 Map 实现(Set 的元素作为 Map 的 key,value 用一个固定的PRESENT
对象填充)。
1. 集合进阶 - 14:HashSet 详解
HashSet
是最常用的 Set 实现类,底层是HashMap,核心特性是 “无序、无重复、允许存 null”。
① 核心成员变量(依赖 HashMap)
public class HashSet<E> extends AbstractSet<E> implements Set<E> {
// 底层存储结构:HashMap(Set的元素作为HashMap的key)
private transient HashMap<E, Object> map;
// 固定的value:所有key对应的value都是这个对象,节省空间
private static final Object PRESENT = new Object();
}
② 核心方法(本质是调用 HashMap 的方法)
HashSet 方法 | 底层调用的 HashMap 方法 | 作用 |
---|---|---|
add(E e) | map.put(e, PRESENT) != null | 添加元素:若 key 不存在(返回 null),则添加成功;若存在(返回旧 value),则添加失败(保证无重复) |
remove(Object o) | map.remove(o) == PRESENT | 删除元素:若 key 存在且 value 是 PRESENT,则删除成功 |
contains(Object o) | map.containsKey(o) | 判断元素是否存在 |
size() | map.size() | 获取元素个数 |
clear() | map.clear() | 清空所有元素 |
③ 关键特性(继承自 HashMap)
- 无序性:元素的存储顺序不保证与添加顺序一致(因为 HashMap 的 key 存储在哈希表中,位置由哈希值决定);
- 无重复性:依赖 HashMap 的 key 不可重复(若两个元素的
hashCode()
相等且equals()
返回 true,则视为同一个元素,添加失败); - 允许存 null:因为 HashMap 的 key 可以为 null(仅允许一个 null,因为 key 不可重复);
- 线程不安全:和 HashMap 一样,无同步锁,多线程修改可能出现并发问题;
- 性能:添加、删除、查询的时间复杂度均为 O (1)(哈希表无冲突时),冲突严重时会退化到 O (n)(但 HashMap 会用红黑树优化,冲突严重时变为 O (logn))。
2. 集合进阶 - 14:LinkedHashSet 详解
LinkedHashSet
是HashSet
的子类,底层是LinkedHashMap,核心特性是 “有序(插入顺序)、无重复”。
① 核心原理(继承自 HashSet,重写底层 Map)
public class LinkedHashSet<E> extends HashSet<E> implements Set<E> {
// 构造方法:调用HashSet的构造方法,传入一个LinkedHashMap
public LinkedHashSet() {
super(new LinkedHashMap<>()); // 底层用LinkedHashMap存储
}
}
LinkedHashMap 的底层是 “哈希表 + 双向链表”:
- 哈希表:保证添加、删除、查询的效率(O (1));
- 双向链表:记录元素的 “插入顺序”,遍历集合时按插入顺序返回(这是 LinkedHashSet “有序” 的根源)。
② 关键特性(对比 HashSet)
特性 | HashSet | LinkedHashSet |
---|---|---|
底层结构 | HashMap(哈希表) | LinkedHashMap(哈希表 + 双向链表) |
有序性 | 无序(哈希值决定顺序) | 有序(按插入顺序) |
无重复性 | 是(依赖 HashMap key 不可重复) | 是(继承自 HashSet) |
允许存 null | 是(仅一个) | 是(仅一个) |
线程安全 | 否 | 否 |
性能 | 略高(无链表维护开销) | 略低(需维护双向链表) |
遍历顺序 | 随机(不固定) | 插入顺序 |
③ 使用场景对比
- 若只需 “无重复、无序”,追求极致性能 → 用
HashSet
; - 若需要 “无重复、有序(插入顺序)”,可接受轻微性能损耗 → 用
LinkedHashSet
; - 若需要 “无重复、有序(自然排序 / 自定义排序)” → 用
TreeSet
(下一节讲解)。
总结与下节预告
到这里,我们已经掌握了:
- 泛型通配符的三种用法(任意类型、上限、下限)及适用场景;
- 红黑树的 5 条规则与自平衡机制(TreeSet/TreeMap 的底层基础);
- HashSet(底层 HashMap,无序无重复)和 LinkedHashSet(底层 LinkedHashMap,有序无重复)的实现与特性。
下一节,我们将讲解TreeSet
(红黑树实现,有序无重复),包括它的两种排序方式(自然排序、自定义排序),并通过综合练习巩固 Set 集合的用法。
上一节我们掌握了泛型通配符、红黑树原理,以及 HashSet/LinkedHashSet 的实现。这一节将聚焦 TreeSet 的排序机制(红黑树的实际应用)、Map 集合的核心用法(双列集合顶层规范),并深入HashMap
和TreeMap
的源码细节,彻底打通 “数据结构→集合实现→实战应用” 的链路。
第七模块:TreeSet—— 基于红黑树的有序 Set
TreeSet
是 Set 集合的有序实现,底层依赖TreeMap
(红黑树),核心特性是 “有序(自然排序 / 自定义排序)、无重复”,本质是通过红黑树的 “二叉查找” 特性保证元素有序,通过 “key 不可重复” 保证无重复。
1. 集合进阶 - 15~16:TreeSet 的两种排序方式
TreeSet 的 “有序” 不是 “插入顺序”,而是 “元素的比较顺序”,分为两种实现方式:自然排序和自定义排序。
① 方式 1:自然排序(实现 Comparable 接口)
自然排序是 TreeSet 的默认排序方式,要求集合中的元素必须实现java.lang.Comparable
接口,并重写compareTo(T o)
方法 —— 该方法定义了元素的 “比较规则”,返回值决定元素的顺序:
- 返回 负数:当前元素排在参数元素的前面;
- 返回 0:当前元素与参数元素 “相等”,TreeSet 会视为同一个元素,不添加;
- 返回 正数:当前元素排在参数元素的后面。
代码案例:自定义对象的自然排序
import java.util.TreeSet;
// 1. 自定义类实现Comparable接口,指定比较类型为Student
class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 2. 重写compareTo方法:按年龄升序排序(年龄相同则按姓名升序)
@Override
public int compareTo(Student o) {
// 第一步:按年龄比较
int ageDiff = this.age - o.age;
if (ageDiff != 0) {
return ageDiff; // 年龄不同:返回差值(正数→当前元素大,排后面)
}
// 第二步:年龄相同,按姓名的字典序比较(String已实现Comparable)
return this.name.compareTo(o.name);
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
public class TreeSetNaturalSort {
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>();
// 添加元素:TreeSet会自动调用compareTo方法排序
treeSet.add(new Student("张三", 20));
treeSet.add(new Student("李四", 18));
treeSet.add(new Student("王五", 20));
treeSet.add(new Student("李四", 18)); // 与已存在的“李四18”相等,添加失败
// 遍历:按年龄升序,年龄相同按姓名升序
for (Student s : treeSet) {
System.out.println(s);
}
// 输出结果:
// Student{name='李四', age=18}
// Student{name='王五', age=20}
// Student{name='张三', age=20}
}
}
② 方式 2:自定义排序(传入 Comparator 比较器)
若元素未实现 Comparable 接口,或想覆盖默认的自然排序规则,可在创建 TreeSet 时传入java.util.Comparator
接口的实现类(或 Lambda 表达式),通过compare(T o1, T o2)
方法定义排序规则:
- 返回 负数:o1 排在 o2 的前面;
- 返回 0:o1 与 o2 相等,不添加;
- 返回 正数:o1 排在 o2 的后面。
代码案例:用 Lambda 实现自定义排序
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetCustomSort {
public static void main(String[] args) {
// 1. 创建TreeSet时传入Comparator(Lambda表达式):按年龄降序排序
TreeSet<Student> treeSet = new TreeSet<>((s1, s2) -> {
// 自定义规则:按年龄降序,年龄相同按姓名降序
int ageDiff = s2.age - s1.age; // 注意:s2 - s1 是降序
if (ageDiff != 0) {
return ageDiff;
}
return s2.name.compareTo(s1.name); // 姓名降序
});
// 添加元素:按自定义规则排序
treeSet.add(new Student("张三", 20));
treeSet.add(new Student("李四", 18));
treeSet.add(new Student("王五", 20));
// 遍历:按年龄降序,年龄相同按姓名降序
for (Student s : treeSet) {
System.out.println(s);
}
// 输出结果:
// Student{name='张三', age=20}
// Student{name='王五', age=20}
// Student{name='李四', age=18}
}
}
③ 关键注意点
- 排序优先级:自定义排序(Comparator)的优先级高于自然排序(Comparable)—— 若同时存在,TreeSet 会优先使用 Comparator;
- 无重复的判定:无论是哪种排序,只要
compareTo
或compare
方法返回 0,TreeSet 就视为元素重复,不会添加; - null 的处理:TreeSet 不允许添加 null 元素(会抛出
NullPointerException
)—— 因为无法对 null 进行比较。
第八模块:Map 集合 —— 双列集合的顶层规范
Map 是 Java 集合框架中的双列集合(存储 “键值对”key-value
),与单列集合(Collection)的核心区别是 “通过 key 定位 value”,key 不可重复(保证唯一性),value 可重复。
1. 集合进阶 - 17(上):Map 的核心特点与常用 API
① Map 的核心特点
- 键值对映射:每个 key 对应一个 value,通过 key 可快速获取 value(类似字典的 “字→释义”);
- key 唯一性:若添加相同 key 的键值对,新的 value 会覆盖旧的 value;
- value 可重复:不同 key 可对应相同的 value;
无序 / 有序:
- 无序:HashMap、HashTable(底层哈希表);
- 有序:LinkedHashMap(按插入顺序)、TreeMap(按 key 排序)。
② Map 的常用 API(以 HashMap 为例)
Map 接口定义了大量操作键值对的方法,核心如下:
import java.util.HashMap;
import java.util.Map;
public class MapApiDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
// 1. 添加键值对:put(key, value),返回旧value(若key不存在则返回null)
map.put("张三", 90);
map.put("李四", 85);
map.put("王五", 95);
Integer oldValue = map.put("张三", 92); // 覆盖“张三”的value,oldValue=90
System.out.println("旧value:" + oldValue); // 输出:旧value:90
// 2. 获取value:get(key),key不存在返回null
Integer score = map.get("李四");
System.out.println("李四的分数:" + score); // 输出:李四的分数:85
// 3. 判断key/value是否存在
boolean hasKey = map.containsKey("王五");
boolean hasValue = map.containsValue(95);
System.out.println("是否包含key'王五':" + hasKey); // 输出:true
System.out.println("是否包含value95:" + hasValue); // 输出:true
// 4. 删除键值对:remove(key),返回被删除的value
Integer removedValue = map.remove("王五");
System.out.println("删除的value:" + removedValue); // 输出:删除的value:95
// 5. 其他常用方法
System.out.println("键值对个数:" + map.size()); // 输出:2
System.out.println("是否为空:" + map.isEmpty()); // 输出:false
map.clear(); // 清空所有键值对
System.out.println("清空后是否为空:" + map.isEmpty()); // 输出:true
}
}
③ Map 的三种遍历方式
Map 没有继承 Iterable 接口,不能直接用增强 for 或迭代器遍历,需通过 “获取 key 集合”“获取键值对集合” 或 “Lambda 表达式” 三种方式遍历:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("张三", 90);
map.put("李四", 85);
map.put("王五", 95);
// 方式1:键找值(先获取key集合,再遍历key找value)
System.out.println("=== 键找值 ===");
Set<String> keySet = map.keySet(); // 获取所有key的Set集合
for (String key : keySet) {
Integer value = map.get(key);
System.out.println(key + ":" + value);
}
// 方式2:键值对(直接获取entry集合,遍历entry)
System.out.println("=== 键值对 ===");
Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); // 获取所有entry的Set集合
for (Map.Entry<String, Integer> entry : entrySet) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ":" + value);
}
// 方式3:Lambda表达式(Java 8+,最简洁)
System.out.println("=== Lambda ===");
map.forEach((key, value) -> System.out.println(key + ":" + value));
}
}
2. 集合进阶 - 17(下)~18:Map 核心实现类详解
Map 的常用实现类有 4 个:HashMap、LinkedHashMap、TreeMap、HashTable,我们重点解析前三个(HashTable 已过时,不推荐使用)。
① HashMap(底层:哈希表 + 红黑树,JDK 8)
HashMap 是 Map 最常用的实现类,核心特性与 HashSet 一致(因为 HashSet 底层是 HashMap):
- 无序:key 的存储顺序由哈希值决定,与插入顺序无关;
- key 允许为 null(仅一个,因为 key 不可重复),value 允许为 null;
- 线程不安全:无同步锁,多线程修改可能出现并发问题(需用
ConcurrentHashMap
替代); - 性能:添加、删除、查询的时间复杂度均为 O (1)(哈希表无冲突时),冲突严重时退化为 O (logn)(红黑树优化)。
源码核心回顾(集合进阶 - 13~16):
- 底层结构:数组(
Node[] table
)+ 链表(Node
节点)+ 红黑树(TreeNode
节点); - 哈希冲突解决:链地址法 —— 相同哈希值的 key 存入同一个数组位置(桶),先形成链表,当链表长度≥8 且数组容量≥64 时,转为红黑树;
扩容机制:
- 初始容量:16(默认),容量始终是 2 的幂次(保证哈希计算时 “取模” 高效,用
(n-1) & hash
替代hash % n
); - 负载因子:0.75(默认)—— 当 “键值对个数 ≥ 数组容量 × 负载因子” 时,触发扩容,扩容为原容量的 2 倍;
- 初始容量:16(默认),容量始终是 2 的幂次(保证哈希计算时 “取模” 高效,用
- key 的哈希计算:
hash(key) = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
—— 将 key 的哈希码高位与低位异或,减少哈希冲突。
② LinkedHashMap(底层:哈希表 + 双向链表)
LinkedHashMap 是 HashMap 的子类,核心特性与 LinkedHashSet 一致:
- 有序:通过双向链表记录 key 的 “插入顺序”,遍历时光标按链表顺序移动,保证输出顺序与插入顺序一致;
- key 允许为 null,value 允许为 null;
- 线程不安全;
- 性能:比 HashMap 略低(需维护双向链表的插入和删除),但遍历效率更高(按链表顺序,无需遍历整个哈希表)。
源码核心:
- 继承自 HashMap,重写了
Node
节点为Entry
节点,新增before
和after
指针,形成双向链表; 新增
accessOrder
参数(默认 false):
accessOrder=false
:按 “插入顺序” 排序(默认);accessOrder=true
:按 “访问顺序” 排序(调用get(key)
后,该 key 会移到链表末尾,适合实现 LRU 缓存)。
③ TreeMap(底层:红黑树)
TreeMap 是 Map 的有序实现类,核心特性与 TreeSet 一致(底层是红黑树):
- 有序:按 key 的 “自然排序” 或 “自定义排序” 排列(中序遍历红黑树可得到有序 key);
- key 不允许为 null(无法比较 null),value 允许为 null;
- 线程不安全;
- 性能:添加、删除、查询的时间复杂度均为 O (logn)(红黑树的平衡特性)。
源码核心回顾(集合进阶 - 17~18):
- 底层结构:红黑树(
Entry
节点构成,每个节点包含 key、value、左子树、右子树、颜色); 排序机制:
- 自然排序:key 必须实现
Comparable
接口,重写compareTo
方法; - 自定义排序:创建 TreeMap 时传入
Comparator
接口实现类,重写compare
方法;
- 自然排序:key 必须实现
- key 的比较:每次添加键值对时,通过比较器确定 key 在红黑树中的位置,保证树的有序性和平衡性。
④ 四种 Map 实现类对比
实现类 | 底层结构 | 有序性 | key 允许 null | value 允许 null | 线程安全 | 时间复杂度 | 适用场景 |
---|---|---|---|---|---|---|---|
HashMap | 哈希表 + 红黑树 | 无序 | 是(1 个) | 是 | 否 | O(1) | 无有序需求,追求高性能 |
LinkedHashMap | 哈希表 + 双向链表 | 插入 / 访问顺序 | 是(1 个) | 是 | 否 | O(1) | 需要保留插入 / 访问顺序 |
TreeMap | 红黑树 | 自然 / 自定义排序 | 否 | 是 | 否 | O(logn) | 需要按 key 有序排列 |
HashTable | 哈希表 | 无序 | 否 | 否 | 是 | O(1) | 已过时,多线程用 ConcurrentHashMap |
总结与下节预告
到这里,我们已经掌握了:
- TreeSet 的两种排序方式(自然排序 Comparable、自定义排序 Comparator)及无重复判定规则;
- Map 集合的核心 API 与三种遍历方式(键找值、键值对、Lambda);
- HashMap、LinkedHashMap、TreeMap 的底层结构、特性与适用场景,以及 HashMap 的源码细节(哈希计算、扩容、红黑树转换)。
下一节,我们将讲解 Map 的工具类(Collections
)、可变参数,以及 6 个综合练习(随机点名器、带权重点名、集合嵌套等),把前面的知识落地到实战场景,解决实际问题。
上一节我们系统掌握了 TreeSet 的排序机制和 Map 核心实现类的原理,这一节将聚焦 集合工具类(Collections)、可变参数 这两个实用特性,并通过 6 个综合练习(随机点名、集合嵌套等)将所有知识落地,帮你彻底打通 “理论→实践” 的最后一公里。
第九模块:集合工具类与可变参数
1. 集合进阶 - 19:可变参数(Variable Arguments)
在实际开发中,经常需要定义 “参数个数不固定” 的方法(比如计算多个数的和),Java 5 引入的 可变参数 可以简化这种场景 —— 它允许方法接收 “任意个数的同类型参数”,本质是 “数组的语法糖”。
① 可变参数的语法与使用
- 语法:在参数类型后加
...
,表示该参数是可变参数,格式为方法名(参数类型... 参数名)
; - 注意:可变参数必须是方法的 最后一个参数,且一个方法只能有一个可变参数。
代码案例:用可变参数计算总和
public class VarargsDemo {
public static void main(String[] args) {
// 调用可变参数方法:可传入任意个数的int参数(0个、1个、多个)
int sum1 = calculateSum(10, 20);
int sum2 = calculateSum(5, 15, 25, 35);
int sum3 = calculateSum(); // 允许传入0个参数
System.out.println("sum1:" + sum1); // 输出:sum1:30
System.out.println("sum2:" + sum2); // 输出:sum2:80
System.out.println("sum3:" + sum3); // 输出:sum3:0
}
// 可变参数方法:计算多个int的总和
public static int calculateSum(int... nums) {
// 可变参数本质是数组,可通过数组语法遍历
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
// 可变参数必须是最后一个参数(正确示例)
public static void printInfo(String name, int... scores) {
System.out.println("姓名:" + name);
System.out.println("成绩:" + java.util.Arrays.toString(scores));
}
// 错误:可变参数不能在非最后位置
// public static void errorMethod(int... nums, String name) {} // 编译报错
}
② 可变参数与集合的结合
可变参数常与 Arrays.asList()
配合,快速将多个元素转为 List 集合(注意:Arrays.asList()
返回的是固定长度的 List,不能调用 add()
/remove()
等修改方法,需转为 ArrayList):
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class VarargsWithCollection {
public static void main(String[] args) {
// 1. 可变参数快速创建List(固定长度)
List<String> fixedList = Arrays.asList("Java", "Python", "Go");
System.out.println(fixedList); // 输出:[Java, Python, Go]
// fixedList.add("C++"); // 错误:UnsupportedOperationException(固定长度)
// 2. 转为可修改的ArrayList
List<String> modifiableList = new ArrayList<>(Arrays.asList("Java", "Python", "Go"));
modifiableList.add("C++");
System.out.println(modifiableList); // 输出:[Java, Python, Go, C++]
// 3. 自定义方法:将可变参数元素添加到集合
addElements(modifiableList, "Ruby", "Swift");
System.out.println(modifiableList); // 输出:[Java, Python, Go, C++, Ruby, Swift]
}
// 可变参数方法:将元素添加到集合
public static <T> void addElements(List<T> list, T... elements) {
for (T elem : elements) {
list.add(elem);
}
}
}
2. 集合进阶 - 20:集合工具类 Collections
java.util.Collections
是操作集合的 工具类,提供了大量静态方法,用于实现集合的排序、查找、同步化等功能(类似 Arrays
操作数组)。核心方法可分为 4 类:
① 排序相关方法
sort(List<T> list)
:对 List 按 自然排序(元素需实现 Comparable);sort(List<T> list, Comparator<? super T> c)
:对 List 按 自定义排序(传入 Comparator);reverse(List<?> list)
:反转 List 中元素的顺序;shuffle(List<?> list)
:随机打乱 List 中元素的顺序(常用在 “洗牌” 场景)。
代码案例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsSort {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
list.add(5);
list.add(4);
// 1. 自然排序(升序)
Collections.sort(list);
System.out.println("自然排序后:" + list); // 输出:[1, 2, 3, 4, 5]
// 2. 自定义排序(降序)
Collections.sort(list, (a, b) -> b - a);
System.out.println("自定义排序(降序)后:" + list); // 输出:[5, 4, 3, 2, 1]
// 3. 反转
Collections.reverse(list);
System.out.println("反转后:" + list); // 输出:[1, 2, 3, 4, 5]
// 4. 随机打乱
Collections.shuffle(list);
System.out.println("打乱后:" + list); // 输出:随机顺序(如[3, 1, 5, 2, 4])
}
}
② 查找与替换相关方法
binarySearch(List<? extends Comparable<? super T>> list, T key)
:对 已排序的 List 进行二分查找,返回元素索引(未找到返回负数);max(Collection<? extends T> coll)
:返回集合中的 最大元素(自然排序);min(Collection<? extends T> coll)
:返回集合中的 最小元素(自然排序);fill(List<? super T> list, T obj)
:用指定元素填充 List(覆盖所有原有元素)。
代码案例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsSearch {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
list.add(40);
list.add(50);
// 1. 二分查找(需先排序,此处已有序)
int index = Collections.binarySearch(list, 30);
System.out.println("30的索引:" + index); // 输出:30的索引:2
// 2. 查找最大/最小元素
Integer max = Collections.max(list);
Integer min = Collections.min(list);
System.out.println("最大值:" + max); // 输出:最大值:50
System.out.println("最小值:" + min); // 输出:最小值:10
// 3. 填充元素
Collections.fill(list, 99);
System.out.println("填充后:" + list); // 输出:[99, 99, 99, 99, 99]
}
}
③ 同步化相关方法(线程安全)
Collections
提供了将 “非线程安全集合” 转为 “线程安全集合” 的方法(底层通过加同步锁实现),但性能较低,推荐用 java.util.concurrent
包下的集合(如 ConcurrentHashMap
)替代:
synchronizedList(List<T> list)
:返回线程安全的 List;synchronizedSet(Set<T> s)
:返回线程安全的 Set;synchronizedMap(Map<K,V> m)
:返回线程安全的 Map。
代码案例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsSynchronized {
public static void main(String[] args) {
// 非线程安全的ArrayList
List<String> unsafeList = new ArrayList<>();
// 转为线程安全的List
List<String> safeList = Collections.synchronizedList(unsafeList);
// 多线程环境下使用safeList,避免并发修改异常
safeList.add("Java");
System.out.println(safeList); // 输出:[Java]
}
}
④ 其他常用方法
emptyList()
/emptySet()
/emptyMap()
:返回空的不可修改集合(避免创建新对象,节省内存);singletonList(T o)
/singletonSet(T o)
/singletonMap(K k, V v)
:返回只包含一个元素的不可修改集合。
本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据