第13项:谨慎地重写clone方法

本文探讨了Java中Cloneable接口的用途和不足,详细解释了如何正确实现clone方法,包括需要处理的异常、字段的复制,以及何时使用构造函数或工厂方法作为替代。还强调了在涉及可变对象时,clone方法的复杂性和潜在问题,以及如何避免这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  Cloneable接口的目的是作为对象的一个mixin接口(mixin interface)(第20项),表明这样的类【的对象】是允许克隆的。遗憾的是,它没有达到这个目的。它的主要缺陷是缺少克隆方法,而Object的clone方法【的访问权限】是受保护【protect】的, 如果不采用反射(第65项),就不能仅仅因为它实现了Cloneable而在对象上调用clone方法。即使是反射调用也可能失败,因为无法保证对象具有可访问的clone方法。尽管存在这样那样的缺陷,这项设施仍然被广泛地使用着,因此值得我们去理解它。该项告诉您如何实现良好的clone方法,并讨论何时适合这样做,并提供替代方法。

  既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法将逐个返回该对象的拷贝字段,否则就会抛出CloneNotSupportedException。这是接口的一种极端非典型的用法,也不值得效仿。通常情况下,实现接口是为了表明类可以为它的客户做些什么。在这种情况下,它会修改超类上受保护方法的行为。

  尽管规范中没有表明,实际上,实现Cloneable的类应该提供一个功能正常的公共克隆方法。 为了达到这个目的,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种脆弱的、危险的、语言之外的(extralinguistic)机制:无需调用构造器就可以创建对象。

  clone方法的通用约定是非常脆弱的,下面是从Object规范中复制的:

  创建和返回该对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式x.clone() != x将会是true,并且,表达式x.clone().getClass() == x.getClass()将会返回true,但这些都不是绝对的要求。虽然通常情况下,表达式x.clone().equals(x)将会是true,但是,这也不是一个绝对的要求。

  按照惯例,此方法返回的对象应该通过调用super.clone来获取。 如果一个类及其所有超类(Object除外)都遵循这个约定,那就是这种情况:

x.clone().getClass() == x.getClass()

  按照惯例,返回的对象应该独立于被克隆的对象。要实现这种独立性,可能需要在返回之前修改super.clone返回的对象的一个或多个字段。

  这个机制与构造函数调用链类似(This mechanism is vaguely similar to constructor chaining)【就是子类的构造函数一定需要调用父类的构造函数】,它只是没有强制执行:如果一个类的clone方法返回一个不是通过super.clone而是通过调用构造函数获得的实例,编译器不会报错(complain),但是如果一个该类的子类调用super.clone,生成的对象将具有错误的类,从而阻止子类clone方法的正常运行。如果重写clone的类是final,则可以安全地忽略此约定,因为没有子类,所以不需要担心。但是如果一个final类有clone方法,但是该方法没有调用super.clone方法,那么该类没有理由实现Cloneable,因为它不依赖与Object的clone实现的行为。

  假设你希望在一个类中实现Cloneable,并且它的超类都提供良好的clone方法。先调用super.clone。您获得的对象将是原始的完整功能的副本。在您的类中声明的任何字段将具有与原始值相同的值。如果每个字段都包含原始值或对不可变对象的引用,则返回的对象可能正是您所需要的,在这种情况下,不需要进一步处理。 例如,对于第11项中的PhoneNumber类就是这种情况,但请注意,不可变类应该永远不会提供clone方法,因为它只会鼓励浪费的复制(because it would merely encourage wasteful copying)【就是说复制了也没啥用,复制的每一个结果都是一样的,因为类是不可变的】。有了这个预告,下面是PhoneNumber的clone方法的样子:

// Clone method for class with no references to mutable state
@Override public PhoneNumber clone() {
   
   
    try {
   
   
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
   
   
        throw new AssertionError(); // Can't happen
    }
}

  为了使此方法起作用,必须修改PhoneNumber的类声明以表明它实现了Cloneable。 虽然Object的clone方法返回Object,但此clone方法返回PhoneNumber。这样做是合法的,也是可取的,因为Java支持协变返回类型(covariant return types)。换句话说,重写方法的返回类型可以是被重写的子类方法的返回类型。这样客户端就不需要进行转换了(This eliminates the need for casting in the client.)。我们必须在返回之前将super.clone的结果从Object转换为PhoneNumber,但是这个转换保证是可以成功的(but the cast is guaranteed to succeed)。

  对super.clone的调用包含在try-catch块中。 这是因为Object声明其clone方法抛出CloneNotSupportedException,这是一个经过检查的异常。 因为PhoneNumber实现了Cloneable,所以我们知道对super.clone的调用会成功。对这个样板文件的需求表明,CloneNotSupportedException应该是不受约束的(第71项)(The need for this boilerplate indicates that CloneNotSupportedException should have been unchecked (Item 71))。

  如果对象包含引用可变对象的字段,使用上述这种简单的clone实现可能会导致灾难性的后果。例如,考虑到第7项中的Stack类:

public class Stack {
   
   
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
   
   
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值