Java 实例 – List 截取(千字长文)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

二级标题:从书架到代码:List 是什么?子列表又是如何工作的?

在 Java 编程中,List 是一种有序的、可重复的数据集合,就像我们书架上排列整齐的书籍,每本书都有自己的位置索引。而“List 截取”则是从这个书架中,选取某一段连续的书籍,形成一个新的子列表。例如,假设书架上有 10 本书,截取第 3 到第 7 本,就会得到一个包含 5 本书的新列表。

为什么需要 List 截取?

在实际开发中,我们经常需要从庞大的数据集中提取特定范围的数据。例如:

  • 分页查询:从数据库获取的 1000 条记录中,截取第 1 到第 10 条作为第一页的数据。
  • 数据过滤:在用户输入的搜索词中,截取前 50 个字符用于显示摘要。
  • 算法优化:在排序或遍历时,仅处理部分数据以提升性能。

二级标题:Java List 截取的核心方法:subList()

在 Java 标准库中,List 接口提供了 subList(int fromIndex, int toIndex) 方法,这是实现 List 截取的核心工具。

方法语法与参数说明

List<ElementType> subList = originalList.subList(fromIndex, toIndex);  
  • fromIndex:截取的起始索引(包含)。
  • toIndex:截取的结束索引(不包含)。
  • 返回值:返回一个新的 List 对象,它是原列表的“视图”而非副本。

关键特性:视图 vs 副本

subList() 返回的是原列表的视图,这意味着:

  1. 修改子列表会影响原列表:如果通过子列表添加或删除元素,原列表也会同步变化。
  2. 动态更新:当原列表的内容发生变化时,子列表会反映这些变化。

比喻:这就像在一张地图上用红笔圈出某个区域,地图本身并没有被复制,只是标记了一个观察的视角。

实例代码演示

以下是一个简单的代码示例:

import java.util.ArrayList;  
import java.util.List;  

public class ListSublistExample {  
    public static void main(String[] args) {  
        List<String> books = new ArrayList<>();  
        books.add("Java 编程思想");  
        books.add("算法导论");  
        books.add("设计模式");  
        books.add("数据结构");  
        books.add("Spring 框架");  

        // 截取索引 1 到 4 的元素(不包含 4)  
        List<String> subList = books.subList(1, 4);  

        System.out.println("子列表内容:" + subList);  
        // 输出:[算法导论, 设计模式, 数据结构]  

        // 修改子列表  
        subList.set(0, "算法精解");  

        // 检查原列表是否变化  
        System.out.println("修改后的原列表:" + books);  
        // 输出:[Java 编程思想, 算法精解, 设计模式, 数据结构, Spring 框架]  
    }  
}  

常见错误与解决方案

错误 1:索引越界

如果 fromIndextoIndex 超出列表长度,会抛出 IndexOutOfBoundsException。例如:

List<String> list = new ArrayList<>(List.of("A", "B"));  
List<String> errorSubList = list.subList(3, 4); // 抛出异常  

解决方案:在调用 subList() 前,先验证索引范围:

if (fromIndex >= 0 && toIndex <= list.size() && fromIndex < toIndex) {  
    // 安全调用 subList()  
}  

错误 2:误解子列表的生命周期

如果原列表被修改(例如通过 clear()add()),子列表可能变得不可用。例如:

List<String> original = new ArrayList<>(List.of("A", "B", "C"));  
List<String> sub = original.subList(1, 2);  
original.add("D"); // 此时 sub 可能失效  

// 后续操作 sub 可能抛出 ConcurrentModificationException  

解决方案:避免在截取后修改原列表的结构。若需修改,建议先将子列表转换为独立列表:

List<String> independentList = new ArrayList<>(sub);  

二级标题:List 截取的扩展场景与最佳实践

场景 1:分页查询的实现

在 Web 开发中,分页是常见的需求。例如,从数据库查询 1000 条记录,每页显示 10 条:

public List<User> getPage(int pageNumber, int pageSize) {  
    List<User> allUsers = repository.findAll();  
    int fromIndex = (pageNumber - 1) * pageSize;  
    return allUsers.subList(fromIndex, fromIndex + pageSize);  
}  

注意:此代码假设 allUsers 已加载到内存中。实际开发中,应优先使用数据库分页(如 SQL 的 LIMIT)以避免内存溢出。

场景 2:安全截取的封装工具类

为避免索引越界,可以封装一个安全的截取工具方法:

public static <T> List<T> safeSubList(List<T> list, int fromIndex, int toIndex) {  
    if (fromIndex < 0 || toIndex > list.size() || fromIndex >= toIndex) {  
        throw new IllegalArgumentException("无效的索引范围");  
    }  
    return list.subList(fromIndex, toIndex);  
}  

场景 3:结合流式处理过滤数据

通过结合 subList() 和 Stream API,可以高效处理数据:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);  
List<Integer> evenNumbers = numbers.subList(1, 9)  
    .stream()  
    .filter(n -> n % 2 == 0)  
    .collect(Collectors.toList());  
// evenNumbers 包含 [2,4,6,8]  

二级标题:性能对比:subList() 与复制操作

关键区别:时间复杂度

  • subList():时间复杂度为 O(1),因为它只是创建了一个对原列表的引用。
  • 复制操作(如 new ArrayList<>(original.subList(...))):时间复杂度为 O(n),需要遍历所有元素。

适用场景

场景推荐方法原因
需要频繁修改原列表subList()直接操作视图,无需复制数据
需要独立的副本复制操作避免原列表变化影响子列表
处理大数据集subList() + 流式处理减少内存占用,提升性能

二级标题:List 截取的常见误区与解决方案

误区 1:“子列表是原列表的浅拷贝”

实际上,subList() 返回的是视图,而非拷贝。因此,修改子列表会直接修改原列表。

解决方案

  • 如果需要独立列表,使用 new ArrayList<>(subList)

误区 2:误用 subList() 的结束索引

例如,试图截取到列表末尾时,容易忘记结束索引应为 list.size()

List<String> lastThree = list.subList(list.size() - 3, list.size()); // 正确  
List<String> wrong = list.subList(list.size() - 3, list.size() - 1); // 错误  

误区 3:与并发修改的冲突

在多线程环境下,若多个线程同时修改原列表和子列表,可能导致 ConcurrentModificationException

解决方案

  • 使用线程安全的集合类(如 CopyOnWriteArrayList)。
  • 在读写操作前加锁。

二级标题:实战案例:实现动态范围查询

案例背景

假设有一个学生管理系统,需要根据分数范围筛选学生记录:

public class Student {  
    private String name;  
    private int score;  
    // 构造函数、Getter/Setter 略  
}  

public List<Student> getStudentsByScoreRange(  
    List<Student> allStudents,  
    int minScore,  
    int maxScore  
) {  
    // 实现逻辑  
}  

解决方案

结合 subList() 和排序:

// 步骤 1:按分数排序  
allStudents.sort(Comparator.comparingInt(s -> s.getScore()));  

// 步骤 2:找到符合条件的起始和结束索引  
int startIndex = 0;  
int endIndex = allStudents.size();  

// 找到第一个 >= minScore 的索引  
while (startIndex < endIndex && allStudents.get(startIndex).getScore() < minScore) {  
    startIndex++;  
}  

// 找到第一个 > maxScore 的索引  
while (endIndex > startIndex && allStudents.get(endIndex - 1).getScore() > maxScore) {  
    endIndex--;  
}  

// 步骤 3:截取子列表  
return allStudents.subList(startIndex, endIndex);  

优化点

  • 预排序:如果列表频繁查询但较少修改,可提前维护一个有序列表。
  • 二分查找:对于大型列表,可用 binarySearch 快速定位索引。

二级标题:结论:掌握 List 截取的底层逻辑与实践技巧

通过本文的讲解,我们深入理解了 subList() 方法的特性、使用场景以及常见问题的解决方案。在实际开发中,应根据需求选择合适的方法:

  • 追求高效:直接使用 subList() 处理视图。
  • 需要独立性:通过复制操作创建副本。
  • 复杂场景:结合排序、流式处理等技术实现高级功能。

掌握 List 截取的核心原理,不仅能提升代码的可读性和性能,还能帮助开发者在数据处理、分页设计等场景中游刃有余。通过不断实践和优化,您将更好地驾驭 Java 中的集合框架,为更复杂的编程任务打下坚实基础。

最新发布