返回

Java初学者不可不看的List.subList方法踩坑小记

后端

揭秘 Java List.subList 方法的陷阱:初学者指南

陷阱一:警惕数组越界

当您使用 List.subList 方法时,务必确保 fromIndex 和 toIndex 参数在 List 范围内。超出范围的索引会导致 IndexOutOfBoundsException 异常。为了避免此陷阱,请务必先检查索引范围。

代码示例:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

// fromIndex 小于 0
try {
    list.subList(-1, 2);
} catch (IndexOutOfBoundsException e) {
    System.out.println("fromIndex 必须非负:" + e.getMessage());
}

// toIndex 大于 list.size()
try {
    list.subList(0, 4);
} catch (IndexOutOfBoundsException e) {
    System.out.println("toIndex 必须小于或等于 list.size():" + e.getMessage());
}

陷阱二:小心堆栈溢出

递归调用 subList 方法可能会导致堆栈溢出异常。如果方法不断创建新的子列表,就会发生这种情况。为了避免此陷阱,请避免递归调用 subList。

代码示例:

public void printSubList(List<Integer> list) {
    if (list.isEmpty()) {
        return;
    }

    // 递归调用
    printSubList(list.subList(1, list.size()));

    System.out.println(list.get(0));
}

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

// 递归调用导致堆栈溢出
printSubList(list);

陷阱三:避免空指针异常

如果您试图在空 List 上调用 subList,或者 resulting子列表为空,您会遇到空指针异常。为了避免此陷阱,请务必先检查 List 是否为 null,并使用 List.isEmpty() 方法检查子列表是否为空。

代码示例:

List<Integer> list = null;

// list 为 null
try {
    list.subList(0, 2);
} catch (NullPointerException e) {
    System.out.println("List 不能为 null:" + e.getMessage());
}

List<Integer> list = new ArrayList<>();

// 子列表为空
try {
    list.subList(0, 2);
} catch (IndexOutOfBoundsException e) {
    System.out.println("子列表为空:" + e.getMessage());
}

陷阱四:警惕内存泄漏

subList 仍然指向原始 List,即使原始 List 被清除。这可能会导致内存泄漏。为了避免此陷阱,请在不再需要时使用 subList.clear() 清除子列表。

代码示例:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

// 创建子列表
List<Integer> subList = list.subList(0, 2);

// 清除原始列表
list.clear();

// 由于 subList 仍然指向原始列表,因此元素不会被垃圾回收

陷阱五:并发安全问题

在多线程环境中使用 subList 时,请注意并发修改异常。如果其他线程修改了原始 List,您可能会遇到此异常。为了避免此陷阱,请在多线程环境中使用 Collections.synchronizedList() 创建一个同步的 List。

代码示例:

List<Integer> list = Collections.synchronizedList(new ArrayList<>());
list.add(1);
list.add(2);
list.add(3);

Thread thread1 = new Thread(() -> {
    // 线程 1 添加元素到原始列表
    list.add(4);
});

Thread thread2 = new Thread(() -> {
    // 线程 2 提取子列表
    List<Integer> subList = list.subList(0, 2);

    // 线程 2 尝试访问子列表中的元素
    System.out.println(subList.get(0));
});

thread1.start();
thread2.start();

最佳实践

遵循这些最佳实践可以避免在使用 List.subList 方法时踩坑:

  • 检查索引范围
  • 避免递归调用
  • 处理空 List 和空子列表
  • 清除不再需要的子列表
  • 在多线程环境中使用同步 List

常见问题解答

  1. 如何检查 fromIndex 和 toIndex 范围?
    您可以使用 List.size() 来获取 List 的长度并检查索引是否在范围内。

  2. 为什么会出现堆栈溢出异常?
    递归调用 subList 方法会导致堆栈溢出异常。避免递归调用可以解决此问题。

  3. 如何避免空指针异常?
    在使用 subList 方法之前,请检查 List 是否为 null 并使用 List.isEmpty() 检查子列表是否为空。

  4. 如何防止内存泄漏?
    在不再需要子列表时使用 subList.clear() 来清除其元素。

  5. 为什么在多线程环境中需要同步 List?
    在多线程环境中,同步 List 可以防止并发修改异常。