Java 实例 – List 截取:从入门到实战
在 Java 开发中,List 是最常用的数据结构之一。无论是处理用户列表、商品数据,还是从数据库中读取结果集,我们几乎每天都在和 List 打交道。但当你面对一个包含上百条数据的 List 时,真的需要全部加载到内存中吗?显然不是。这时,“截取”操作就显得尤为重要。
想象一下,你正在开发一个电商平台的“商品列表页”,后台返回了 1000 条商品数据,但前端每页只显示 20 条。如果不做截取,不仅浪费内存,还可能导致页面卡顿甚至崩溃。这时,掌握 Java 实例 – List 截取 的技巧,就是提升程序性能的关键一步。
本文将带你系统学习 Java 中如何对 List 进行高效截取,涵盖多种常用场景和最佳实践,适合初学者快速上手,也适合中级开发者查漏补缺。
什么是 List 截取?
List 截取,简单来说,就是从一个大的 List 中提取出指定范围的元素,形成一个新的子列表。这在分页、数据预览、数据筛选等场景中极为常见。
比如你有一个包含 100 个用户的 List,现在只想获取第 11 到第 20 个用户,这就是典型的截取操作。Java 提供了多种方式实现这一功能,最核心的是 List.subList() 方法,它来自 java.util.List 接口。
⚠️ 注意:
subList()返回的是原 List 的视图(view),并非独立副本。这意味着对子列表的修改会直接影响原列表,这一点必须格外小心。
使用 subList() 方法实现基础截取
subList(int fromIndex, int toIndex) 是最直接、最高效的截取方式。它接受两个参数:
fromIndex:起始索引(包含)toIndex:结束索引(不包含)
import java.util.ArrayList;
import java.util.List;
public class ListSubListExample {
public static void main(String[] args) {
// 创建一个包含 10 个元素的 List
List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
fruits.add("葡萄");
fruits.add("草莓");
fruits.add("芒果");
fruits.add("西瓜");
fruits.add("哈密瓜");
fruits.add("樱桃");
fruits.add("桃子");
// 截取第 2 到第 5 个元素(索引从 0 开始)
// 索引 1 到 4(包含 1,不包含 5)
List<String> slicedFruits = fruits.subList(1, 5);
// 输出截取结果
System.out.println("截取结果:" + slicedFruits);
// 输出:截取结果:[香蕉, 橙子, 葡萄, 草莓]
}
}
✅ 说明:
fruits.subList(1, 5)表示从索引 1 开始,到索引 5 结束(不包含 5)- 该方法时间复杂度为 O(1),性能极高,因为它不创建新对象,仅返回视图
- 但务必注意:后续对
fruits的结构性修改(如 add/remove)可能导致slicedFruits抛出ConcurrentModificationException
如何安全地获取独立副本?
由于 subList() 返回的是视图,若你希望得到一个独立、不受原列表影响的副本,就需要显式复制。
方法一:使用构造函数创建新 List
List<String> originalList = new ArrayList<>();
originalList.add("Java");
originalList.add("Python");
originalList.add("Go");
originalList.add("Rust");
originalList.add("TypeScript");
// 截取索引 1 到 3(包含 1,不包含 4)
List<String> subList = originalList.subList(1, 4);
// 创建独立副本,避免后续修改影响原列表
List<String> independentCopy = new ArrayList<>(subList);
System.out.println("独立副本:" + independentCopy);
// 输出:独立副本:[Python, Go, Rust]
✅ 优势:简单直接,适合大多数场景
⚠️ 注意:如果原 List 很大,复制会消耗额外内存和时间
方法二:使用 Stream API(推荐用于现代 Java)
Java 8 引入的 Stream API 让 List 截取更加优雅。
import java.util.List;
import java.util.stream.Collectors;
List<String> languages = List.of("Java", "Python", "Go", "Rust", "TypeScript", "Kotlin");
// 使用 Stream 实现截取:跳过前 2 个,取接下来的 3 个
List<String> slicedLanguages = languages.stream()
.skip(2) // 跳过前 2 个元素
.limit(3) // 限制最多取 3 个
.collect(Collectors.toList());
System.out.println("Stream 截取结果:" + slicedLanguages);
// 输出:Stream 截取结果:[Go, Rust, TypeScript]
✅ 优势:代码更清晰,支持链式调用,且返回的是独立副本
✅ 适合处理不可变列表(如List.of())或需要复杂逻辑的截取
实际案例:分页查询中的 List 截取
在 Web 开发中,分页是最常见的需求之一。假设你从数据库获取了 1000 条用户数据,前端每页显示 10 条。
我们来模拟一个分页工具类:
import java.util.ArrayList;
import java.util.List;
public class PaginationUtil {
/**
* 分页截取方法
* @param allData 所有数据
* @param pageSize 每页大小
* @param pageNum 页码(从 1 开始)
* @return 当前页数据
*/
public static <T> List<T> paginate(List<T> allData, int pageSize, int pageNum) {
// 计算起始索引(页码从 1 开始,所以减 1)
int fromIndex = (pageNum - 1) * pageSize;
// 计算结束索引(不能超过总长度)
int toIndex = Math.min(fromIndex + pageSize, allData.size());
// 使用 subList 截取,再转换为独立副本
return new ArrayList<>(allData.subList(fromIndex, toIndex));
}
public static void main(String[] args) {
// 模拟 100 条用户数据
List<String> users = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
users.add("用户" + i);
}
// 获取第 2 页数据(每页 10 条)
List<String> page2 = paginate(users, 10, 2);
System.out.println("第 2 页数据:" + page2);
// 输出:第 2 页数据:[用户11, 用户12, 用户13, 用户14, 用户15, 用户16, 用户17, 用户18, 用户19, 用户20]
}
}
✅ 关键点:
Math.min(fromIndex + pageSize, allData.size())确保不会越界- 最终返回
new ArrayList<>(...)保证独立性- 该方法可复用于任何 List 类型(泛型支持)
截取时的常见错误与避坑指南
即使掌握了基本用法,开发者仍可能踩坑。以下是几个典型问题:
错误 1:索引越界
List<String> list = List.of("A", "B", "C");
list.subList(1, 5); // 抛出 IndexOutOfBoundsException
❌ 原因:索引 5 超出范围(最大索引为 2)
✅ 解决:使用Math.min(toIndex, list.size())限制结束位置
错误 2:修改原列表导致异常
List<String> list = new ArrayList<>(List.of("X", "Y", "Z"));
List<String> sub = list.subList(0, 2);
list.add("W"); // 修改原列表
sub.get(0); // 可能抛出 ConcurrentModificationException
❌ 原因:
subList是视图,原列表被修改后,视图状态失效
✅ 解决:在截取后立即复制为独立列表,或避免在使用视图期间修改原列表
性能对比:不同方法的效率分析
| 方法 | 时间复杂度 | 内存消耗 | 是否独立副本 | 适用场景 |
|---|---|---|---|---|
subList() |
O(1) | 低 | 否 | 需要视图,临时使用 |
new ArrayList<>(subList()) |
O(n) | 中 | 是 | 多次使用,避免影响原列表 |
Stream.skip().limit() |
O(n) | 中 | 是 | 复杂逻辑,链式操作 |
💡 建议:
- 性能优先:使用
subList(),但注意避免修改原列表- 安全性优先:使用
new ArrayList<>(subList())或 Stream- 代码可读性优先:Stream 更适合复杂业务逻辑
总结与建议
Java 实例 – List 截取 并不是什么高深技巧,但却是日常开发中不可或缺的基本功。从 subList() 的高效视图机制,到 Stream 的函数式风格,再到分页场景的实战封装,每一步都体现了对数据处理的掌控力。
记住几个关键原则:
subList()快速但危险,用完即弃,不要随意修改原列表- 如需独立副本,务必显式复制
- 分页场景中,合理使用
skip()和limit()更加安全 - 保持代码清晰,优先选择可读性高的方案
无论你是刚入门 Java 的学生,还是已有几年经验的开发者,掌握 List 截取,都能让你的代码更健壮、更高效。下一次当你面对一个超大 List 时,别再一股脑全加载,试试截取吧——它可能就是性能瓶颈的突破口。
希望这篇文章能帮你打通 List 截取的任督二脉。如果觉得有收获,不妨点个赞,分享给还在为分页烦恼的小伙伴。