首页 > 技术文章 > 对象克隆

mybdss 2021-06-20 22:47 原文

什么是对象克隆

要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用。这说明, 任何一个变量改变都会影响另一个变量。

Employee original = new Employee("John Public", 50000);
Employee copy = original ;
copy.raiseSalary(10); // 也改变了原来的

如果希望copy 是一个新对象,它的初始状态与original 相同, 但是之后它们各自会有自己不同的状态, 这种情况下就可以使用clone 方法。

Employee copy = original,clone();
copy.raiseSalary(10); // 没有改变原来的

image-20210620105029262

浅拷贝

不过并没有这么简单。clone 方法是Object 的一个protected 方法, 这说明你的代码不能直接调用这个方法。只有Employee 类可以克隆Employee 对象。这个限制是有原因的。想想看Object 类如何实现clone。它对于这个对象一无所知, 所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来, 原对象和克隆的对象仍然会共享一些信息。

为了更直观地说明这个问题, 下图显示了使用Object 类的clone 方法克隆这样一个Employee 对象会发生什么。可以看到, 默认的克隆操作是“ 浅拷贝”,并没有克隆对象中引用的其他对象。

浅拷贝会有什么影响吗? 这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的, 那么这种共享就是安全的。如果子对象属于一个不可变的类, 如String, 就是这种情况。或者在对象的生命期中, 子对象一直包含不变的常量, 没有更改器方法会改变它, 也没有方法会生成它的引用,这种情况下同样是安全的。

image-20210620105720533

深拷贝

不过, 通常子对象都是可变的, 必须重新定义clone 方法来建立一个深拷贝, 同时克隆所有子对象。在这个例子中,hireDay 域是一个Date, 这是可变的, 所以它也需要克隆。

对于每一个类,需要确定:

  1. 默认的clone 方法是否满足要求;
  2. 是否可以在可变的子对象上调用clone 来修补默认的clone 方法;
  3. 是否不该使用clone()

实际上第3 个选项是默认选项。如果选择第1 项或第2 项,类必须:

  1. 实现Cloneable 接口;
  2. 重新定义clone 方法,并指定public 访问修饰符。

Object 类中clone 方法声明为protected , 所以你的代码不能直接调用anObject.clone()。 但是, 不是所有子类都能访问受保护方法吗? 不是所有类都是Object 的子类吗? 幸运的是, 受保护访问的规则比较微妙。子类只能调用受保护的clone方法来克隆它自己的对象。必须重新定义clone 为public 才能允许所有方法克隆对象。

受保护的访问

大家都知道, 最好将类中的域标记为private, 而方法标记为public。任何声明为private的内容对其他类都是不可见的。前面已经看到, 这对于子类来说也完全适用, 即子类也不能访问超类的私有域。
然而,在有些时候,人们希望超类中的某些方法允许被子类访问, 或允许子类的方法访问超类的某个域。为此, 需要将这些方法或域声明为protected。例如, 如果将超类Employee中的hireDay 声明为proteced, 而不是私有的, Manager 中的方法就可以直接地访问它。不过, Manager 类中的方法只能够访问Manager 对象中的hireDay 域, 而不能访问其他Employee 对象中的这个域。这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利。
在实际应用中,要谨慎使用protected 属性。假设需要将设计的类提供给其他程序员使用, 而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下, 如果需要对这个类的实现进行修改, 就必须通知所有使用这个类的程序员。这违背了OOP 提倡的数据封装原则。
受保护的方法更具有实际意义。如果需要限制某个方法的使用, 就可以将它声明为protected。这表明子类(可能很熟悉祖先类)得到信任, 可以正确地使用这个方法, 而其他类则不行。

在这里, Cloneable 接口的出现与接口的正常使用并没有关系。具体来说, 它没有指定clone 方法,这个方法是从Object 类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很“ 偏执”, 如果一个对象请求克隆, 但没有实现这个接口, 就会生成一个受査异常。

Cloneable 接口是Java 提供的一组标记接口(tagging interface) 之一。(有些程序员称之为记号接口(marker interface))。应该记得,Comparable 等接口的通常用途是确保一个类实现一个或一组特定的方法。标记接口不包含任何方法; 它唯一的作用就是允许在类型查询中使用instanceof:

if (obj instanceof Cloneable) . . .
建议你自己的程序中不要使用标记接口。

即使clone 的默认(浅拷贝)实现能够满足要求, 还是需要实现Cloneable 接口, 将clone重新定义为public, 再调用super.clone()。 下面给出一个例子:

class Employee implements Cloneable {
    // 将可见性级别提高到public,更改返回类型
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone();
    }
}

在Java SE 1.4 之前, clone 方法的返回类型总是Object, 而现在可以为你的clone方法指定正确的返回类型。这是协变返回类型的一个例子。

协变:我们知道, 方法的名字和参数列表称为方法的签名。例如, f(int) 和f(String)是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。不过, 返回类型不是签名的一部分, 因此, 在覆盖方法时, 一定要保证返回类型的兼容性。允许子类将覆盖方法的返回类型定义为原返回类型的子类型。例如, 假设Employee 类有
public Employee getBuddy() { . . . }
经理不会想找这种地位低下的员工。为了反映这一点, 在后面的子类Manager 中,可以按照如下所示的方式覆盖这个方法
public Manager getBuddy() { . . . } // 更改返回类型
我们说, 这两个getBuddy 方法具有可协变的返回类型。

与Object.clone 提供的浅拷贝相比, 前面看到的clone 方法并没有为它增加任何功能。这里只是让这个方法是公有的。要建立深拷贝,还需要做更多工作,克隆对象中可变的实例域。

下面来看创建深拷贝的clone 方法的一个例子:

class Employee implements Cloneable {

    public Employee clone() throws CloneNotSupportedException {
        // 调用 Object的clone()方法
        Employee cloned = (Employee) super.clone();
        // 克隆可变字段
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }
}

如果在一个对象上调用clone, 但这个对象的类并没有实现Cloneable 接口, Object 类的clone 方法就会拋出一个CloneNotSupportedException。 当然,Employee 和Date 类实现了Cloneable 接口, 所以不会抛出这个异常。不过, 编译器并不了解这一点,因此,我们声明了这个异常:

public Employee done() throws CloneNotSupportedException

捕获这个异常是不是更好一些?

public Employee clone() {
    try {
    	Employee cloned = (Employee) super.clone();
    }catch (CloneNotSupportedException e) {
        return null;
    }
    // 这不会发生,因为我们是可克隆的
}

这非常适用于final 类。否则, 最好还是保留throws 说明符。这样就允许子类在不支持克隆时选择抛出一个CloneNotSupportedException。

必须当心子类的克隆。例如, 一旦为Employee 类定义了clone 方法, 任何人都可以用它来克隆Manager 对象。Employee 克隆方法能完成工作吗? 这取决于Manager 类的域。在这里是没有问题的, 因为bonus 域是基本类型。但是Manager 可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正clone 方法让它正常工作。出于这个原因, 在Object类中clone 方法声明为protected。不过, 如果你希望类用户调用clone, 就不能这样做。

要不要在自己的类中实现clone 呢? 如果你的客户需要建立深拷贝,可能就需要实现这个方法。有些人认为应该完全避免使用clone, 而实现另一个方法来达到同样的目的。clone相当笨拙, 这一点我们也同意,不过如果让另一个方法来完成这个工作, 还是会遇到同样的问题。毕竟, 克隆没有你想象中那么常用。标准库中只有不到5% 的类实现了clone。

以下是LinkedList实现的clone:

@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
    try {
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}

/**
 * Returns a shallow copy of this {@code LinkedList}. (The elements
 * themselves are not cloned.)
 *
 * @return a shallow copy of this {@code LinkedList} instance
 */
public Object clone() {
    LinkedList<E> clone = superClone();

    // Put clone into "virgin" state
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    // Initialize clone with our elements
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

所有数组类型都有一个public 的clone 方法, 而不是protected。可以用这个方法建立一个新数组, 包含原数组所有元素的副本。例如:

int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
int[] cloned = luckyNumbers.clolne();
cloned[5] = 12; // 不改变luckyNumbers[5]

参考资料

《Java核心技术 卷1 基础知识(原书第10版)》

推荐阅读