前言

本章介绍 Java 的对象克隆,并给出常见的使用案例。

版本约定

克隆是原始对象的精确拷贝。在 Java 中,它本质上意味着能够创建一个与原始对象状态相似的对象。Java 的 clone() 方法提供了这个功能。

默认情况下,Java 的克隆是“逐字段复制”(field by field copy),也就是说,Object 类并不了解将被调用的 clone() 方法的类的结构。

因此,JVM 在进行克隆时,会做以下事情。

  1. 如果类只有原始数据类型的成员,那么将创建一个全新的对象副本,并返回对新对象副本的引用。
  2. 如果类包含任何引用类型的成员,那么只有这些成员的对象引用被复制,因此原始对象和克隆对象中的成员引用都指向同一个对象。

除了上述默认行为外,你可以随时覆盖这一行为,并指定你自己的行为。这可以通过覆盖 clone() 方法来实现。让我们看看它是如何做到的。

使用 Cloneable 接口和 clone() 方法实现克隆

每种支持克隆对象的语言都有自己的规则,Java 也是如此。在 Java 中,如果一个类需要支持克隆,它必须要做以下事情。

  1. 你必须实现 Cloneable 接口。
  2. 你必须覆盖 Object 类中的 clone() 方法(这很奇怪,Clone() 方法应该在 Cloneable 接口中)。

Cloneable 接口没有指定 clone 方法,这个方法是从 Object 类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很“偏执”,如果一个对象请求克隆,但没有实现这个接口,就会生成一个受查异常。

根据上面的规则,即使 clone 的默认(浅拷贝)实现能够满足要求,还是需要实现 Cloneable 接口,将 clone 重新定义为 public,再调用 super.clone()。

关于 clone() 方法的 Java 文档如下所示:

  1. /**
  2. * Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
  3. * The general intent is that, for any object x, the expression:
  4. * 1) x.clone() != x, will be true.
  5. * 2) x.clone().getClass() == x.getClass(), will be true, but these are not absolute requirements.
  6. * 3) x.clone().equals(x), will be true, this is not an absolute requirement.
  7. */
  8. protected native Object clone() throws CloneNotSupportedException;
  1. 第一条语句保证了克隆的对象将有独立的内存地址分配。
  2. 第二条语句建议原始对象和克隆对象应该具有相同的类型,但这并不是强制性的。
  3. 第三条语句建议原始对象和克隆对象在使用 equals() 方法比较的时候,应该是相等的,但这并不是强制性的。

让我们通过例子来理解 Java 克隆。我们的第一个类是 Employee 类,有 3 个属性:id,name 和 department。

  1. public class Employee implements Cloneable {
  2. private int id;
  3. private String name;
  4. private Department department;
  5. public Employee(int id, String name, Department dept) {
  6. this.id = id;
  7. this.name = name;
  8. this.department = dept;
  9. }
  10. @Override
  11. public Object clone() throws CloneNotSupportedException {
  12. return super.clone();
  13. }
  14. // Getters and Setters
  15. }

Department 类有两个属性:id 和 name。

  1. public class Department {
  2. private int id;
  3. private String name;
  4. public Department(int id, String name) {
  5. this.id = id;
  6. this.name = name;
  7. }
  8. // Getters and Setters
  9. }

因此,如果我们需要克隆 Employee 类,那么我们需要执行如下操作。

  1. public class TestCloning {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Department dept = new Department(1, "Human Resource");
  4. Employee original = new Employee(1, "Admin", dept);
  5. // Lets create a clone of original object
  6. Employee cloned = (Employee) original.clone();
  7. // Let verify using employee id, if cloning actually workded
  8. System.out.println("cloned.getId(): " + cloned.getId());
  9. // Verify JDK's rules
  10. // Must be true and objects must have different memory addresses
  11. System.out.println("original != cloned: " + (original != cloned));
  12. // As we are returning same class; so it should be true
  13. System.out.println("original.getClass() == cloned.getClass(): " + (original.getClass() == cloned.getClass()));
  14. // Default equals method checks for references so it should be false. If we want to make it true,
  15. // then we need to override equals method in Employee class.
  16. System.out.println("original.equals(cloned): " + original.equals(cloned));
  17. }
  18. }

运行程序,输出:

  1. cloned.getId(): 1
  2. original != cloned: true
  3. original.getClass() == cloned.getClass(): true
  4. original.equals(cloned): false

我们成功地克隆了 Employee 对象。但是,department 对象只是引用被复制,现在来改变克隆对象中 department 对象的状态,看看是否会对原始对象有影响。

  1. public class TestCloning {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Department hr = new Department(1, "Human Resource");
  4. Employee original = new Employee(1, "Admin", hr);
  5. Employee cloned = (Employee) original.clone();
  6. // Let change the department name in cloned object and we will verify in original object
  7. cloned.getDepartment().setName("Finance");
  8. System.out.println("original.getDepartment().getName(): " + original.getDepartment().getName());
  9. System.out.println("cloned.getDepartment().getName(): " + cloned.getDepartment().getName());
  10. }
  11. }

运行程序,输出:

  1. original.getDepartment().getName(): Finance
  2. cloned.getDepartment().getName(): Finance

通过上面的测试,我们知道克隆对象的变化在原始对象中也是可见的。这样,如果允许克隆对象,克隆对象可能会在系统中造成严重破坏。任何人都可以来克隆应用程序中的对象,并执行他喜欢的任何操作。我们能防止这种情况吗?

答案是肯定的,我们可以通过 Java 深拷贝来防止这种情况。让我们首先看看 Java 中的深拷贝和浅拷贝是什么。

Java 浅拷贝

Java 默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。在重载的 clone 方法中,如果你没有克隆所有的引用类型,那么你就是在做一个浅拷贝。

以上所有的例子都是浅拷贝,因为我们没有在 Employee 类的克隆方法中克隆 Department 对象。

下图显示了 Employee 对象的浅拷贝会发生什么。
对象克隆 - 图1
浅拷贝会有什么影响?这样看具体情况。如果原对象和浅拷贝对象共享的子对象是不可变的,那么这种共享就是安全的。比如,子对象属于一个不可变的类(如 String),这种情况是安全的。或者在对象的生命期中,子对象是一个不可变的常量,没有构造方法能改变它,也没有方法可以生成它的引用,这种情况下同样是安全的。

Java 深拷贝

不过,通常子对象都是可变的,必须重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。

在深拷贝中,我们创建了一个独立于原始对象的克隆,对克隆对象的改变不应影响原始对象。

让我们看看在 Java 中是如何创建深度拷贝的。

  1. // Modified clone() method in Employee class
  2. @Override
  3. public Object clone() throws CloneNotSupportedException {
  4. Employee cloned = (Employee) super.clone();
  5. cloned.department = (Department) department.clone();
  6. return cloned;
  7. }

我修改了 Employee 类的 clone() 方法,并在 Department 类中加入了以下的 clone 方法。

  1. // Defined clone method in Department class.
  2. @Override
  3. public Object clone() throws CloneNotSupportedException {
  4. return super.clone();
  5. }

现在测试我们的克隆代码,可以得到理想的结果,部门的名称不会被修改。

  1. public class TestCloning {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Department hr = new Department(1, "Human Resource");
  4. Employee original = new Employee(1, "Admin", hr);
  5. Employee cloned = (Employee) original.clone();
  6. // Let change the department name in cloned object and we will verify in original object
  7. cloned.getDepartment().setName("Finance");
  8. System.out.println("original.getDepartment().getName(): " + original.getDepartment().getName());
  9. System.out.println("cloned.getDepartment().getName(): " + cloned.getDepartment().getName());
  10. }
  11. }

运行程序,输出:

  1. original.getDepartment().getName(): Human Resource
  2. cloned.getDepartment().getName(): Finance

改变克隆对象的状态并不影响原始对象。

因此,深度克隆需要满足以下规则:

  • 不需要单独复制原始类型。
  • 原始类中的所有成员类都应支持克隆,并且在上下文中原始类的克隆方法中应在所有成员类上调用super.clone()。
  • 如果任何成员类不支持克隆,则在 clone 方法中,必须创建一个该成员类的新实例,并将其所有属性逐个复制到新的成员类对象。此新成员类对象将被设置在克隆的对象中。

    使用构造器实现克隆

拷贝构造函数是类中的特殊构造函数,它接受的参数是自己的类类型。因此,当你把一个类的实例传递给复制构造函数时,构造函数将返回一个新的类的实例,其值是从参数实例中复制的。它可以帮助你通过 Cloneable 接口来克隆对象。

让我们来看个例子。

  1. public class PointOne {
  2. private Integer x;
  3. private Integer y;
  4. public PointOne(PointOne point) {
  5. this.x = point.x;
  6. this.y = point.y;
  7. }
  8. // Other construct
  9. // Getters and Setters
  10. }

这种方法看起来很简单,直到继承。当你通过扩展父类来定义一个类时,还需要在那里定义一个类似的构造函数。在子类中,你需要复制子类的特定属性,并将参数传递给超类的构造函数。让我们看看是如何实现的?

  1. public class PointTwo extends PointOne {
  2. private Integer z;
  3. public PointTwo(PointTwo point) {
  4. super(point); //Call Super class constructor here
  5. this.z = point.z;
  6. }
  7. // Other construct
  8. // Getters and Setters
  9. }

这样实现克隆是存在一定问题的。比如,在上面的例子中,如果我们将 PointTwo 的实例传递给 PointOne 的构造函数,在这种情况下,会得到一个 PointOne 的实例。

  1. public class Test {
  2. public static void main(String[] args) {
  3. PointOne one = new PointOne(1, 2);
  4. PointTwo two = new PointTwo(1, 2, 3);
  5. PointOne clone1 = new PointOne(one);
  6. PointOne clone2 = new PointOne(two);
  7. //Let check for class types
  8. System.out.println(clone1.getClass());
  9. System.out.println(clone2.getClass());
  10. }
  11. }

运行程序,输出:

  1. class test15.PointOne
  2. class test15.PointOne

创建复制构造函数的另一种方法是拥有静态工厂方法。它们在参数中接受类的类型,并使用该类的另一个构造函数创建一个新的实例。然后这些工厂方法会将所有的状态数据复制到上一步刚刚创建的新类实例中,并返回这个新的实例。

  1. public class PointOne implements Cloneable {
  2. private Integer x;
  3. private Integer y;
  4. public PointOne(Integer x, Integer y) {
  5. this.x = x;
  6. this.y = y;
  7. }
  8. public PointOne copyPoint(PointOne point) throws CloneNotSupportedException {
  9. if (!(point instanceof Cloneable)) {
  10. throw new CloneNotSupportedException("Invalid cloning");
  11. }
  12. // Can do multiple other things here
  13. return new PointOne(point.x, point.y);
  14. }
  15. }

使用 Java 拷贝构造器克隆对象是比较繁琐的,特别是当类的字段比较多时更加麻烦,不推荐使用该方式。

使用序列化实现深拷贝

序列化是深度克隆的另一种简单方法。在这个方法中,你只需将要克隆的对象序列化,然后将其反序列化。很明显,需要克隆的对象应该实现 Serializable 接口。

这种技术不能轻易使用,它存在如下问题。

  1. 首先,序列化性能不高。它可能很容易比 clone() 方法多消耗 100 倍时间。
  2. 其次,不是所有的对象都是可序列化的。
  3. 第三,让一个类成为可序列化(Serializable)的类是很棘手的,并不是所有的类都能靠它来完成。

    1. @SuppressWarnings("unchecked")
    2. public static <T extends Serializable> T clone(T t) throws Exception {
    3. // Serialize it
    4. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    5. ObjectOutputStream oos = new ObjectOutputStream(baos);
    6. oos.writeObject(t);
    7. oos.close();
    8. // Deserialize it and return the new instance
    9. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    10. ObjectInputStream ois = new ObjectInputStream(bais);
    11. T clone = (T) ois.readObject();
    12. ois.close();
    13. return clone;
    14. }

    不需要我们自己实现序列化方法,在 Apache commons 中,SerializationUtils 类已经提供了使用序列化实现深拷贝的方法。

    1. <dependency>
    2. <groupId>org.apache.commons</groupId>
    3. <artifactId>commons-lang3</artifactId>
    4. <version>3.7</version>
    5. </dependency>
    1. SomeObject cloned = SerializationUtils.clone(someObject);

    总结

  • 首先,对象克隆推荐使用 Java 自带的 clone() 方法,性能较高,只是需要我们自己实现深拷贝功能。
  • 其次,在不考虑性能的前提下,也可以使用第三方工具 SerializationUtils 类提供的使用序列化实现深拷贝的方法。
  • 另外,在下篇文章中介绍的对象复制工具 Orika,也是比较推荐的,性能相比其他的一些工具会好很多。
  • 所有数组类型都有一个 public 的 clone 方法,可以用这个方法克隆一个新数组,包含原数组所有元素的副本,需要注意的是,数组的克隆是浅拷贝。

    参考

  • Java 核心技术 卷1 基础知识 第10版

  • Java clone – deep and shallow copy – copy constructors

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/emnayh 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。