如何克隆ArrayList及其内容?

309

我该如何在Java中克隆一个ArrayList并且同时克隆它的元素?

比如我有以下代码:

ArrayList<Dog> dogs = getDogs();
ArrayList<Dog> clonedList = ....something to do with dogs....

我期望clonedList中的对象与dogs列表中的对象不相同。


这个问题已经在深度克隆实用工具推荐的讨论中提及过了。 - Swiety
21个回答

219

个人而言,我会为Dog添加一个构造函数:

class Dog
{
    public Dog()
    { ... } // Regular constructor

    public Dog(Dog dog) {
        // Copy all the fields of Dog.
    }
}

那么就像Varkhan的答案所示,只需迭代:

public static List<Dog> cloneList(List<Dog> dogList) {
    List<Dog> clonedList = new ArrayList<Dog>(dogList.size());
    for (Dog dog : dogList) {
        clonedList.add(new Dog(dog));
    }
    return clonedList;
}

我发现这种方法的优点是你不需要在Java中破解Cloneable。它也符合你复制Java集合的方式。

另一个选择是编写自己的ICloneable接口并使用它。这样,您可以编写通用的克隆方法。


能否为未定义的对象编写该函数(而不是狗)? - Tobi G.
@TobiG。我不明白你的意思。你是想要 cloneList(List<Object>) 还是 Dog(Object) - cdmckay
@cdmckay 有一个函数可以用于cloneList(List<Object>)、cloneList(List<Dog>)和cloneList(List<Cat>)。但是我猜你不能调用泛型构造函数,对吧? - Tobi G.
@TobiG。像一个通用的克隆函数吗?这并不是这个问题所关注的内容。 - cdmckay
正如@helpermethod所述,这是一种常见的模式,既干净又易于理解。 - milosmns
显示剩余2条评论

217

您需要逐个迭代这些项,并将它们克隆,然后在克隆的同时将其放入结果数组中。

public static List<Dog> cloneList(List<Dog> list) {
    List<Dog> clone = new ArrayList<Dog>(list.size());
    for (Dog item : list) clone.add(item.clone());
    return clone;
}

显然,为了使此方法生效,您需要让Dog类实现Cloneable接口并重写clone()方法。


24
然而,你不能通用地这样做。clone()不是Cloneable接口的一部分。 - Michael Myers
15
但是clone()方法在Object类中是受保护的,因此你无法访问它。尝试编译该代码。 - Michael Myers
6
所有的类都继承自Object,因此它们可以重写clone()方法。这就是Cloneable的作用! - Stephan202
3
这是一个好的回答。Cloneable实际上是一个接口。然而,mmyers的观点是正确的,因为clone()方法是在Object类中声明的受保护方法。你需要在你的Dog类中重写这个方法,并手动复制字段。 - Jose
3
我建议创建一个工厂或构造器,甚至只是一个静态方法,它可以接收一个Dog实例,手动将其各个字段复制到一个新的实例中,并返回该新实例。 - Jose
显示剩余10条评论

155

所有标准集合都有拷贝构造函数。请使用它们。

List<Double> original = // some list
List<Double> copy = new ArrayList<Double>(original); //This does a shallow copy

clone()存在一些设计错误(参见此问题),因此最好避免使用它。

来自《Effective Java 第二版》,第11条: 明智地覆盖clone方法

考虑到与Cloneable相关的所有问题,可以肯定地说,其他接口都不应该扩展它,而专为继承而设计的类(Item 17)也不应该实现它。由于其许多缺点,一些专家程序员选择从不覆盖clone方法,也从不调用它,除了可能要复制数组。如果你为继承而设计一个类,请注意,如果你不提供行为良好的受保护的clone方法,那么子类将无法实现Cloneable。

这本书还描述了拷贝构造函数相对于Cloneable/clone的许多优点。

  • 它们不依赖于风险高的语言外对象创建机制
  • 他们不要求遵守文档稀少的惯例
  • 他们不会与final字段的正确使用冲突
  • 他们不会抛出不必要的受检异常
  • 它们不需要转换。

考虑使用拷贝构造函数的另一个好处:假设您有一个HashSet s,并且您想将其复制为一个TreeSet。clone方法无法提供此功能,但使用转换构造函数很容易:new TreeSet(s)


106
据我所知,标准集合类的拷贝构造函数创建的是“浅拷贝”,而不是“深拷贝”。这里提出的问题要求深拷贝的答案。 - Abdull
27
这是完全错误的,复制构造函数只进行浅拷贝- 这道题的重点就在于此。 - NimChimpsky
2
这个答案的正确之处在于,如果您没有改变列表中的对象,则添加或删除项目不会将它们从两个列表中删除。它并不像简单赋值那样浅。 - Noumenon

57

Java 8提供了一种新的、优雅而简洁的方法来调用元素dogs的复制构造函数或克隆方法:lambda表达式收集器

复制构造函数:

List<Dog> clonedDogs = dogs.stream().map(Dog::new).collect(toList());

表达式 Dog::new 被称为 方法引用。它创建一个函数对象,该对象调用接受另一只狗作为参数的 Dog 构造函数。 克隆方法 [1]:
List<Dog> clonedDogs = dogs.stream().map(Dog::clone).collect(toList());

获取一个ArrayList作为结果

或者,如果你需要获取一个ArrayList(以便稍后进行修改):

ArrayList<Dog> clonedDogs = dogs.stream().map(Dog::new).collect(toCollection(ArrayList::new));

原地更新列表

如果您不需要保留dogs列表的原始内容,可以使用replaceAll方法并在原地更新列表:

dogs.replaceAll(Dog::new);

所有的例子都假设import static java.util.stream.Collectors.*;

ArrayList的收集器

上一个例子中的收集器可以制作成一个工具方法。由于这是一个非常常见的操作,我个人喜欢让它变得简短美观。就像这样:

ArrayList<Dog> clonedDogs = dogs.stream().map(d -> d.clone()).collect(toArrayList());

public static <T> Collector<T, ?, ArrayList<T>> toArrayList() {
    return Collectors.toCollection(ArrayList::new);
}

[1] 关于CloneNotSupportedException的注意事项:

为了使此解决方案有效,Dog类的clone方法不能声明它抛出CloneNotSupportedException异常。原因是map方法的参数不允许抛出任何已检查的异常。

示例代码如下:

    // Note: Method is public and returns Dog, not Object
    @Override
    public Dog clone() /* Note: No throws clause here */ { ...

这应该不是一个大问题,因为那是最佳实践。 (例如,《Effective Java》给出了这个建议。)
感谢Gustavo指出这一点。

1
@SaurabhJinturkar:你的版本不是线程安全的,不应该与并行流一起使用。这是因为“parallel”调用使得clonedDogs.add被多个线程同时调用。使用collect的版本是线程安全的。这是流库函数模型的优点之一,相同的代码可以用于并行流。 - Lii
1
@SaurabhJinturkar:此外,collect操作速度很快。它基本上与您的版本执行相同的操作,但也适用于并行流。您可以通过使用并发队列而不是数组列表来修复您的版本,但我几乎可以肯定那样会慢得多。 - Lii
@YogenRai:不,情况并非如此。你的代码行会给出列表的浅拷贝;结果将是一个新列表,但它将包含相同的“Dog”对象。 “源列表”不会被修改,但任何将来对内容狗的修改都将修改与源中相同的狗。这个问题是关于在新列表中创建新的狗对象。这就是所谓的深拷贝 - Lii
@Lii Ya 你说得对 :) 我没有注意到在 dogs 中的任何元素中的更改,反之在克隆列表中的更改将反映在两个列表中。而且,正如其他人指出的那样,我认为复制构造函数是创建新副本的最佳选项。所以,我们的lambda表达式将是 List<Dog> clonedList = dogs.stream().map(Dog::new).collect(Collectors.toList()); ,我在 Dog 类中有复制构造函数。不管怎样,谢谢。 - Yogen Rai
@YogenRai:你说得对,使用复制构造函数会更好。我会将其添加到答案中。 - Lii
显示剩余7条评论

30

基本上有三种方法可以避免手动迭代,

1 使用构造函数

ArrayList<Dog> dogs = getDogs();
ArrayList<Dog> clonedList = new ArrayList<Dog>(dogs);

2 使用 addAll(Collection<? extends E> c)

ArrayList<Dog> dogs = getDogs();
ArrayList<Dog> clonedList = new ArrayList<Dog>();
clonedList.addAll(dogs);

3 使用带有 int 参数的 addAll(int index, Collection<? extends E> c) 方法

ArrayList<Dog> dogs = getDogs();
ArrayList<Dog> clonedList = new ArrayList<Dog>();
clonedList.addAll(0, dogs);

NB:如果在操作正在进行时修改了指定的集合,则这些操作的行为将是未定义的。


58
请注意,这3个变体只创建列表的浅拷贝 - electrobabe
9
这不是深度克隆,这两个列表保留相同的对象,只是复制了引用,但是 Dog 对象一旦被修改,任何一个列表改变后另一个列表都会有相同的变化。不知道为什么会有这么多赞。 - Saorikido
2
@Neeson.Z 所有方法都会创建列表的深拷贝和元素的浅拷贝。如果您修改列表中的一个元素,更改将反映在另一个列表中,但是如果您修改其中一个列表(例如删除对象),则另一个列表将保持不变。 - Alessandro Teruzzi

17

我认为当前的绿色答案不好,你可能会问为什么?

  • 它可能需要添加大量的代码
  • 它要求您列出要复制的所有列表并执行此操作

我认为序列化的方式也很差,您可能需要在各个地方添加可序列化的标记。

那么解决方案是什么:

Java深度克隆库 这个克隆库 是一个小型的、开源的(Apache许可证)Java库,可以深度克隆对象。对象不必实现Cloneable接口。实际上,这个库可以克隆任何Java对象。例如,在缓存实现中,如果您不希望缓存对象被修改,或者想要创建对象的深层副本时,可以使用它。

Cloner cloner=new Cloner();
XX clone = cloner.deepClone(someObjectOfTypeXX);

请查看https://github.com/kostaskougios/cloning


8
使用这种方法的一个注意事项是它使用了反射,这可能比Varkhan的解决方案慢得多。 - cdmckay
6
我不理解第一个观点"需要很多代码"。你所谈论的库需要更多的代码,这只是放置位置的问题。否则,我同意为此类事情使用特殊库是有帮助的。 - nawfal

13
您可以使用JSON(与JSON库一起)对列表进行序列化和反序列化。当反序列化时,序列化的列表不引用原始对象。 使用Google GSON:
List<CategoryModel> originalList = new ArrayList<>(); // add some items later
String listAsJson = gson.toJson(originalList);
List<CategoryModel> newList = new Gson().fromJson(listAsJson, new TypeToken<List<CategoryModel>>() {}.getType());

你还可以使用其他的JSON库,比如Jackson。

使用这种方法的优点是,你可以在不必创建类、接口和克隆逻辑的情况下解决问题(如果你的对象内部还有其他对象列表,则可能会非常冗长)。


1
我不知道为什么有人对这个答案进行了负投票。其他的答案要么需要实现clone()方法,要么需要更改依赖项以包含新库。但是JSon库大多数项目都已经包含了。我为此点了赞。 - Satish
1
@Satish 是的,这是唯一一个对我有帮助的答案。我不确定其他人的方法是否有问题,但是无论我做了什么,克隆或使用拷贝构造函数,我的原始列表都会被更新,但是这种方法不会,所以感谢作者! - Parag Pawar
1
嗯,确实这不是为了知识而纯粹的Java回复,但这是一个快速解决此问题的有效方案。 - marcRDZ
1
伟大的黑客,节省了很多时间。 - m.yuki

5

我一直使用这个选项:

ArrayList<Dog> clonedList = new ArrayList<Dog>(name_of_arraylist_that_you_need_to_Clone);

2
你需要手动克隆 ArrayList (通过迭代它并将每个元素复制到新的 ArrayList 中),因为 clone() 不会为你完成。原因是 ArrayList 中包含的对象本身可能不实现 Clonable编辑:...这正是 Varkhan 的代码所做的。

1
即使它们这样做了,除了反射之外,没有访问clone()的方法,而且也不能保证成功。 - Michael Myers

2

其他一些将ArrayList作为深拷贝复制的替代方法

替代方法1 - 使用外部包commons-lang3,方法SerializationUtils.clone()

SerializationUtils.clone()

假设我们有一个名为dog的类,该类的字段是可变的,并且至少有一个字段是String类型的对象和可变的 - 不是原始数据类型(否则浅拷贝就足够了)。

浅拷贝示例:

List<Dog> dogs = getDogs(); // We assume it returns a list of Dogs
List<Dog> clonedDogs = new ArrayList<>(dogs);

现在回到狗的深拷贝。

狗类只有可变字段。

狗类:

public class Dog implements Serializable {
    private String name;
    private int age;

    public Dog() {
        // Class with only mutable fields!
        this.name = "NO_NAME";
        this.age = -1;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

请注意,Dog类实现了Serializable接口!这使得我们可以利用方法“SerializationUtils.clone(dog)”进行克隆。
阅读主方法中的注释以理解结果。它表明我们已成功地制作了ArrayList()的深层副本。请参见下文中上下文中的“SerializationUtils.clone(dog)”。
public static void main(String[] args) {
    Dog dog1 = new Dog();
    dog1.setName("Buddy");
    dog1.setAge(1);

    Dog dog2 = new Dog();
    dog2.setName("Milo");
    dog2.setAge(2);

    List<Dog> dogs = new ArrayList<>(Arrays.asList(dog1,dog2));

    // Output: 'List dogs: [Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]'
    System.out.println("List dogs: " + dogs);

    // Let's clone and make a deep copy of the dogs' ArrayList with external package commons-lang3:
    List<Dog> clonedDogs = dogs.stream().map(dog -> SerializationUtils.clone(dog)).collect(Collectors.toList());
    // Output: 'Now list dogs are deep copied into list clonedDogs.'
    System.out.println("Now list dogs are deep copied into list clonedDogs.");

    // A change on dog1 or dog2 can not impact a deep copy.
    // Let's make a change on dog1 and dog2, and test this
    // statement.
    dog1.setName("Bella");
    dog1.setAge(3);
    dog2.setName("Molly");
    dog2.setAge(4);

    // The change is made on list dogs!
    // Output: 'List dogs after change: [Dog{name='Bella', age=3}, Dog{name='Molly', age=4}]'
    System.out.println("List dogs after change: " + dogs);

    // There is no impact on list clonedDogs's inner objects after the deep copy.
    // The deep copy of list clonedDogs was successful!
    // If clonedDogs would be a shallow copy we would see the change on the field
    // "private String name", the change made in list dogs, when setting the names
    // Bella and Molly.
    // Output clonedDogs:
    // 'After change in list dogs, no impact/change in list clonedDogs:\n'
    // '[Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]\n'
    System.out.println("After change in list dogs, no impact/change in list clonedDogs: \n" + clonedDogs);
}

输出:

List dogs: [Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]
Now list dogs are deep copied into list clonedDogs.
List dogs after change: [Dog{name='Bella', age=3}, Dog{name='Molly', age=4}]
After change in list dogs, no impact/change in list clonedDogs:
[Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]

评论:由于在更改列表dogs后,列表clonedDogs没有影响/更改,因此ArrayList的深层复制成功了!

备选方案2 - 不使用外部包的方法:

在Dog类中引入了一个新方法“clone()”,并删除了与备选方案1相比的“implements Serializable”。

clone()

狗类:

public class Dog {
    private String name;
    private int age;

    public Dog() {
        // Class with only mutable fields!
        this.name = "NO_NAME";
        this.age = -1;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /**
     * Returns a deep copy of the Dog
     * @return new instance of {@link Dog}
     */
    public Dog clone() {
        Dog newDog = new Dog();
        newDog.setName(this.name);
        newDog.setAge(this.age);
        return newDog;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

阅读下面主方法中的注释以了解结果。它显示我们已成功制作了ArrayList()的深层副本。请参见下面的"clone()"方法:

public static void main(String[] args) {
    Dog dog1 = new Dog();
    dog1.setName("Buddy");
    dog1.setAge(1);

    Dog dog2 = new Dog();
    dog2.setName("Milo");
    dog2.setAge(2);

    List<Dog> dogs = new ArrayList<>(Arrays.asList(dog1,dog2));

    // Output: 'List dogs: [Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]'
    System.out.println("List dogs: " + dogs);

    // Let's clone and make a deep copy of the dogs' ArrayList:
    List<Dog> clonedDogs = dogs.stream().map(dog -> dog.clone()).collect(Collectors.toList());
    // Output: 'Now list dogs are deep copied into list clonedDogs.'
    System.out.println("Now list dogs are deep copied into list clonedDogs.");

    // A change on dog1 or dog2 can not impact a deep copy.
    // Let's make a change on dog1 and dog2, and test this
    // statement.
    dog1.setName("Bella");
    dog1.setAge(3);
    dog2.setName("Molly");
    dog2.setAge(4);

    // The change is made on list dogs!
    // Output: 'List dogs after change: [Dog{name='Bella', age=3}, Dog{name='Molly', age=4}]'
    System.out.println("List dogs after change: " + dogs);

    // There is no impact on list clonedDogs's inner objects after the deep copy.
    // The deep copy of list clonedDogs was successful!
    // If clonedDogs would be a shallow copy we would see the change on the field
    // "private String name", the change made in list dogs, when setting the names
    // Bella and Molly.
    // Output clonedDogs:
    // 'After change in list dogs, no impact/change in list clonedDogs:\n'
    // '[Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]\n'
    System.out.println("After change in list dogs, no impact/change in list clonedDogs: \n" + clonedDogs);
}

输出:

List dogs: [Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]
Now list dogs are deep copied into list clonedDogs.
List dogs after change: [Dog{name='Bella', age=3}, Dog{name='Molly', age=4}]
After change in list dogs, no impact/change in list clonedDogs:
[Dog{name='Buddy', age=1}, Dog{name='Milo', age=2}]

评论: 在更改列表狗后,克隆狗列表没有任何影响/更改, 因此ArrayList的深拷贝是成功的!

注意1: 替代方案1比替代方案2慢得多, 但更容易维护,因为您不需要更新任何方法,如clone()。

注意2:对于替代方案1,使用了以下maven依赖项来执行方法“SerializationUtils.clone()”:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

在以下网址中查找更多的common-lang3版本:

https://mvnrepository.com/artifact/org.apache.commons/commons-lang3


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接