杰瑞科技汇

java list的 clone

  1. clone() 方法的本质和来源
  2. List 接口与 AbstractList 的实现
  3. ArrayListclone() 具体行为
  4. LinkedListclone() 具体行为
  5. 为什么通常不推荐使用 clone()
  6. 推荐的替代方案(深拷贝和浅拷贝)

clone() 方法的本质和来源

在 Java 中,clone() 方法不是由 List 接口定义的,而是所有 Java 对象的“祖先”——java.lang.Object 类中的一个方法。

java list的 clone-图1
(图片来源网络,侵删)
// 位于 Object 类中
protected native Object clone() throws CloneNotSupportedException;

关键点:

  • native:表示这个方法不是用 Java 实现的,而是由 JVM 直接在底层 C++ 代码中实现的。
  • protected:这意味着只有同一个包内的类或者其子类才能直接调用它,如果你有一个 List 引用,你是不能直接调用 list.clone() 的,因为 List 接口没有暴露这个方法。
  • Object.clone() 的默认行为是“浅拷贝”(Shallow Copy):它会创建一个新对象,然后将原始对象中所有“字段”的值复制到新对象中,对于基本数据类型(int, double 等),这没问题,但对于对象引用,它只是复制了引用地址,而不是引用所指向的对象本身,这会导致新旧对象共享同一个内部对象,从而可能引发意外问题。

List 接口与 AbstractList 的实现

由于 Object.clone()protected 的,List 接口本身当然不会有 clone() 方法,那么为什么我们有时又能看到 list.clone() 呢?

这是因为 List 的主要实现类,如 ArrayListLinkedList,都继承自 AbstractList,而 AbstractList 类重写了 clone() 方法并将其设为 public

// 位于 AbstractList 类中
public Object clone() {
    try {
        // 调用 Object 的 native clone 方法
        @SuppressWarnings("unchecked")
        AbstractList<?> v = (AbstractList<?>) super.clone();
        // ... 其他处理 ...
        return v;
    } catch (CloneNotSupportedException e) {
        // 这实际上永远不会发生,因为 AbstractList 实现了 Cloneable
        throw new InternalError(e);
    }
}

为了使 super.clone() 能够成功,类必须实现 java.lang.Cloneable 接口,这个接口是一个“标记接口”(Marker Interface),它本身不包含任何方法,只是告诉 JVM:“这个对象可以被克隆”。

java list的 clone-图2
(图片来源网络,侵删)

ArrayListLinkedList 都实现了 Cloneable 接口,所以它们的 clone() 方法可以被调用。


ArrayListclone() 具体行为(浅拷贝)

ArrayList 内部使用一个数组(elementData)来存储元素,它的 clone() 方法执行了以下操作:

  1. 调用 super.clone() 创建一个新的 ArrayList 实例,这个新实例的 size 和原始对象一样。
  2. 关键一步:它将原始 ArrayList 内部数组 elementData 的引用直接复制给了新对象的 elementData

这意味着什么?

新创建的 ArrayList 对象本身是一个新的实例,但它内部的元素数组与原始 ArrayList 共享。

让我们用代码和图示来说明:

import java.util.ArrayList;
import java.util.List;
public class ArrayListCloneExample {
    public static void main(String[] args) {
        // 1. 创建原始列表并添加元素
        List<String> originalList = new ArrayList<>();
        originalList.add("Apple");
        originalList.add("Banana");
        // 2. 克隆列表
        // 注意:这里需要将 List 强制转换为 ArrayList 才能调用 clone()
        List<String> clonedList = (ArrayList<String>) ((ArrayList<String>) originalList).clone();
        // 3. 修改原始列表
        originalList.set(0, "Orange"); // 修改第一个元素
        // 4. 修改克隆列表的内部对象(如果元素是可变的)
        // 假设我们有一个可变的元素
        MutableElement element = new MutableElement("Initial");
        originalList.add(element);
        // 通过原始列表修改这个可变对象
        element.setValue("Modified via Original");
        // 5. 观察结果
        System.out.println("Original List: " + originalList); // [Orange, Banana, MutableElement{value='Modified via Original'}]
        System.out.println("Cloned List:   " + clonedList);   // [Apple, Banana, MutableElement{value='Modified via Original'}]
        // 结论1:修改原始列表中的 String(不可变对象)不影响克隆列表
        // 结论2:修改原始列表和克隆列表共同引用的可变对象,会影响彼此
    }
    static class MutableElement {
        String value;
        public MutableElement(String value) { this.value = value; }
        public void setValue(String value) { this.value = value; }
        @Override public String toString() { return "MutableElement{value='" + value + "'}"; }
    }
}

内存示意图

从图中可以看出:

  • originalListclonedList 是两个不同的 ArrayList 对象。
  • 它们内部的 elementData 数组指向的是同一个内存地址
  • originalList 的结构性修改(如 add, remove)不会影响 clonedListsize,但如果修改了数组中某个位置的元素(特别是可变对象),这个变化会同时反映在两个列表中。

LinkedListclone() 具体行为

LinkedListclone() 行为与 ArrayList 类似,也是浅拷贝

LinkedList 内部使用 Node 对象(包含 item, next, prev)来构成链表。clone() 方法会遍历原始链表,为每个元素创建一个新的 Node 对象,但新 Node 中的 item 字段(即列表中的元素)是直接从原始 Node 复制过来的引用。

LinkedList 的克隆结果也是一个新的 LinkedList 实例,但其节点中的元素与原始列表的节点共享引用。


为什么通常不推荐使用 clone()

  1. 混淆的语义clone() 的行为是“浅拷贝”,但这与很多开发者直觉上的“克隆”(即完全独立的副本)相悖,人们期望的是深拷贝。
  2. 受保护的设计Object.clone()protected 的,这本身就是一个设计信号,表明它不应该被随意作为公共 API 使用。
  3. Cloneable 接口的设计缺陷: Joshua Bloch 在《Effective Java》中称 Cloneable 接口的设计是“一个巨大的错误”,它没有定义 clone() 方法,却改变了 Object.clone() 的行为(从抛出异常到成功),这种基于接口的异常机制非常反直觉。
  4. 性能和不确定性:克隆过程的具体实现依赖于各个类,可能比其他创建副本的方式更慢或更不可预测。

推荐的替代方案

既然 clone() 不推荐,那么我们应该如何创建 List 的副本呢?根据需求选择“浅拷贝”或“深拷贝”。

构造函数(推荐用于浅拷贝)

这是最简单、最清晰、最受推荐的创建浅拷贝的方法。

List<String> originalList = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 创建一个与 originalList 内容相同的新列表
List<String> shallowCopy = new ArrayList<>(originalList);
// 或者
List<String> shallowCopy2 = originalList.stream().collect(Collectors.toList());

优点

  • 代码清晰易懂,意图明确。
  • 不依赖于任何可能被废弃或行为怪异的 clone() 机制。
  • 适用于所有标准的 Collection 实现类。

手动实现深拷贝

当你需要创建一个深拷贝(即列表中的所有元素也都是独立的副本)时,必须手动实现。

假设你有一个 Person 类:

class Person {
    private String name;
    private int age;
    // constructor, getters, setters
}

深拷贝示例

List<Person> originalList = new ArrayList<>();
originalList.add(new Person("Alice", 30));
originalList.add(new Person("Bob", 25));
// 手动创建深拷贝
List<Person> deepCopy = new ArrayList<>(originalList.size());
for (Person person : originalList) {
    // 为每个元素创建一个新实例并复制数据
    // 假设 Person 有一个拷贝构造函数
    deepCopy.add(new Person(person.getName(), person.getAge()));
    // 或者使用 setter
    // Person newPerson = new Person();
    // newPerson.setName(person.getName());
    // newPerson.setAge(person.getAge());
    // deepCopy.add(newPerson);
}
// 修改 originalList 中的任何 Person 对象都不会影响 deepCopy
originalList.get(0).setName("Alice Smith");
System.out.println(deepCopy.get(0).getName()); // 输出仍然是 "Alice"

如果列表中的元素本身也包含可变对象,那么你需要递归地进行拷贝,这会变得非常复杂,这时可以考虑使用序列化/反序列化的方式来实现通用的深拷贝工具类(但要注意性能和对象必须实现 Serializable 接口的问题)。

方法 类型 优点 缺点 适用场景
list.clone() 浅拷贝 API 直接 语义模糊、设计有缺陷、不推荐 几乎不推荐使用,除非你非常清楚其行为且维护旧代码。
new ArrayList<>(list) 浅拷贝 代码清晰、意图明确、推荐 只能实现浅拷贝 创建 List 浅拷贝的首选方法。
手动循环拷贝 深拷贝 完全控制拷贝逻辑 代码冗长,需要为每个类实现拷贝逻辑 当你需要深拷贝且元素类型可控时。

核心建议忘记 Listclone() 方法吧,在 99% 的情况下,使用构造函数 new ArrayList<>(originalList) 来创建一个浅拷贝的列表副本,这是最安全、最清晰、最符合 Java 编程习惯的做法,如果需要深拷贝,则手动实现拷贝逻辑。

分享:
扫描分享到社交APP
上一篇
下一篇