Effective Java 【对于所有对象都通用的方法】第13条 谨慎地覆盖clone

本文探讨了Cloneable接口的作用,其在对象克隆中的局限性,以及如何在不同场景下重写和优化clone方法,包括数组、非数组对象、线程安全和继承类的情况。提倡使用构造器或工厂方法替代,以避免与final域冲突和提升代码灵活性。
摘要由CSDN通过智能技术生成

谨慎地覆盖clone

Cloneable接口地目的是作为对象的一个mixin接口(详见第20条),表示这样的对象允许克隆(clone)。遗憾的是,他并没有成功地达到这个目的。它的主要缺陷在于缺少一个clone方法,而Object的clone方法是受保护的。如果不借助反射方法。即使是反射调用也可能失败,因为不能保证对象一定具有可访问的clone方法。尽管存在缺陷,这项设施仍然被广泛使用,因此值得我们进一步了解。本条目将告诉你如何实现一个行为良好的clone方法,并讨论何时适合这样做,同时也简单的讨论了其他的可替代做法

Cloneable接口并没有包含任何方法,那么它到底有什么作用呢?

他决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就会返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedExcepton异常。这是接口的一种极端极端非典型的用法,也不值得效仿。通常情况下,实现接口是为了表示类可以为它的客户做些什么。然而,对于Cloneable接口,他改变了超类中受保护的方法的行为。

简而言之:Cloneable接口标志了这个类可以调用自身或者超类中的clone()方法,如果没有申明实现该接口则会抛出CloneNotSupportedExcepton异常。

Object类中的clone()方法

Object最为所有类的超类,它的clone()方法这样定义:创建并返回一个对象的拷贝。

clone 方法是浅拷贝,对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存,相对应的深拷贝则会连引用的对象也重新创建。

也就是说对于值类型clone()方法会直接copy一份新的到克隆对象中,但是对于引用类型,两个克隆对象中某一对象的引用还是指向内存中同一个地址。这往往会代码许多灾难性的后果。

所以如果要利用clone方法进行克隆出良好的对象的话,需要讲克隆对象中的引用类型逐一克隆

如何重写好一个clone()方法

其实要点就在于需要对克隆对象域中的引用域进行逐一克隆,下面给出几个关键注意点:

1.对于数组类型我可以采用clone()方法的递归

数组类:

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];

        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    // Ensure space for at least one more element.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

clone()方法:

public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

实际上,clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中地约束条件。

还要注意如果elements域是final的,上述方案就不能正常工作,因为clone方法是被禁止给final域赋新值的。这是个根本问题:就像序列化一样,Cloneable架构与引用可变对象的final域的正常用法是不相兼容的。除非在原始对象和克隆对象之间可以安全地共享此可变对象。为了使类成为可克隆地,可能有必要从某些域中去掉final修饰符。

2.如果对象是非数组,建议提供拷贝构造器(copy constructor)或者拷贝工厂(copy factory)

clone方法对于数组来说其实可以简单递归调用clone达到克隆的效果。
但如果对象在复杂一点,比如涉及到散列表,eg:HashMap,此时clone方法会变得十分复杂。

其实clone方法无法是构造器的另一种方式,参考这种行为我们完全可以提供一个专门拷贝对象的构造器或者工厂方法,只需要传入被克隆的兑现即可。
eg:

public Yum(Yum yum){......}

拷贝构造器的做法,及其静态工厂方法的变形,都比Cloneable/clone方法具有更多的优势:

  1. 它们不依赖于某一种很有风险的、语言之外的创建对象机制;
  2. 它们不要求遵守尚未制定好的文档规范;
  3. 它们不会与final域的正常使用发生冲突;
  4. 它们不会抛出不必要的受检异常;
  5. 它们不需要进行类型转换。

甚至,拷贝构造器或者拷贝工厂可以带一个参数,参数类型是该类所实现的接口。例如,按照惯例所有通用集合实现都提供一个拷贝构造器,其参数类型是Collection或者Map接口。基于接口的拷贝构造器和拷贝工厂(更准确的叫法应该是转换构造器)和转换工厂,允许客户选择拷贝的实现类型,而不是强迫客户接收原始的实现类型。例如,假设你有一个HashSet:s,并且希望把它拷贝成一个TreeSet。clone方法无法提供这样的功能,但是转换构造器很容易实现:new TreeSet<>(s)。

3.如果为线程安全的类重写clone()方法

为了保证一个对象克隆出来的两个对象是完全不一样的,我们需要给clone()方法加上同步才行。
eg:

public Object synchronized clone(){
....
}

4.如果为需要被继承的类重写clone()方法

为继承(详见第19条)设计类有两种选择,但是无论选择其中的哪一种方法,这个类都不应该实现Cloneable接口。你可以选择模拟Object的行为:实现一个功能适当的受保护的clone方法,他应该被声明抛出CloneNotSupportedException异常。这样可以使子类具有实现或者不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样,或者,也可以选择不去实现一个有效的clone方法,并防止子类去实现它,只需要提供下列退化了的clone实现即可:

// clone method for extendable class not supporting Cloneable
@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

总结

所有的问题都是与Cloneable接口有关,新的接口就不应该扩展这个接口,新的可扩展的类也不应该实现这个接口。虽然final类实现Cloneable接口没有太大危害,这个应该被视同性能优化,留到少数必要的情况下才使用(详见第67条),总之,复制功能最好由构造器或者工厂提供。这条规则最绝对的例外是数组,最好利用clone方法复制数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值