第十一条:谨慎地覆盖clone

Clone方法的通用约定非常弱:

创建和返回该对象的一个拷贝。这个拷贝的精确含义取决于该对象的类。一般含义是,对于任何对象x,表达式
x.clone() != x
将会是true,并且,表达式
x.clone().getClass() == x.getClass()
将会是true,但这些都不是绝对的要求,虽然通常情况下,表达式
x.clone().equals(x)
将会是true,但是,这也不是一个绝对的要求。拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程中没有调用构造器。

如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。

如何实现一个行为良好的clone方法:

第一种情况:如果类的每个域包含一个基本数据类型的值,或者包含一个指向不可变对象的引用,那么直接调super.clone()方法即可。
在这里插入图片描述

完整代码如下:

/**
 * @description:hashCode
 * @author: lty
 * @date: 2021/5/15 23:25
 */
public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(short areaCode, short prefix, short lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }
    private static void rangeCheck(int arg,int max,String name) {
        if(arg < 0 || arg > max)
            throw new IllegalArgumentException(name +": "+ arg);
    }

    @Override
    public boolean equals(Object obj) {
        //1、使用==操作符检查“参数是否为这个对象的引用”
        if(obj == this)
            return true;
        //2、使用instanceof操作符检查“参数是否为正确的类型”
        if(!(obj instanceof PhoneNumber))
            return false;
        //3、把参数转化成正确的类型
        PhoneNumber pn = (PhoneNumber)obj;
        //4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        //把某个非零常数值,比如说17,保存在一个名为result的int类型的变量值中
        int result = 17;
        //如果该域是byte、char、short 或者int 类型,则计算(int)f;
        //把步骤2.a中计算得到的散列码c 合并到result 中:
        // result = 31 * result + c;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    @Override
    public String toString() {
        return "PhoneNumber{" +
                "areaCode=" + areaCode +
                ", prefix=" + prefix +
                ", lineNumber=" + lineNumber +
                '}';
    }

    @Override
    protected PhoneNumber clone() throws CloneNotSupportedException {
        return (PhoneNumber)super.clone();
    }
}

第二种情况:如果对象中包含的域引用了可变的对象,对于可变对象要递归地调用clone方法。
例如:

/**
 * @description:栈
 * @author: lty
 * @date: 2021/5/16 17:18
 */
public class Stack {
    //可变的对象
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

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

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

    private void ensureCapacity() {
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2*size+1);
        }
    }

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

    @Override
    protected Stack clone() throws CloneNotSupportedException {
        Stack result = (Stack)super.clone();
        //对于可变对象要递归地调用clone方法
        result.elements = elements.clone();
        return result;
    }
}

实际上,clone方法就是另一个构造器:你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。为了使Stack类中的clone方法正常地工作,它必须要浅拷贝栈的内部信息,最容易的方法就是,在elements数组中递归地调用clone。

注意:如果elements域是final的,上述方法就不能正常工作。

clone架构与引用可变对象的final域的正常用法是不相兼容的。

第三种情况:如果被克隆的可变对象,通过浅拷贝,使得拷贝后的对象与原始对象是一样的(引用都一样),可能需要写一个深度拷贝的方法,拷贝那个可变对象,指向新对象的引用代替原来指向这些对象的引用。
例如:

/**
 * @description:散列表
 * @author: lty
 * @date: 2021/5/16 17:39
 */
public class HashTable implements Cloneable {
    //它是链表的头节点
    private Entry[] buckets = ...;
    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        Entry deepCopy() {
            Entry result = new Entry(key, value, next);
            for (Entry p = result; p.next != null; p = p.next) {
                p.next = new Entry(p.next.key, p.next.value, p.next.next);
            }
            return result;
        }

    }

    /*@Override
    public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            //这个clone数组引用的链表与原始对象是一样的,
            // 从而容易引起克隆对象和原始对象中不确定的行为。
            result.buckets = buckets.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }*/

    @Override
    public HashTable clone() throws CloneNotSupportedException {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i=0; i < buckets.length; i++) {
                if (buckets[i] != null) {
                    result.buckets[i] = buckets[i].deepCopy();
                }
            }
            return result;
    }
}

或者是,先调用super.clone,然后把结果对象中的所有域都设置成它们的空白状态,然后调用高层的方法来重新产生对象的状态。

注意:

1、如同构造器一样,clone方法不应该在构造的过程中,调用新对象中任何非final的方法。

2、如果你决定用线程安全的类来实现Cloneable接口,要记得它的clone方法必须得到很好的同步,就像任何其他方法一样。

总结:所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。

对象拷贝:

第一种:如果你扩展一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。

第二种:提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类。(更好)

例如:

public Yum(Yum yun);

拷贝工厂是类似于拷贝构造器的静态工厂:

public static Yum newInstance(Yum yum);

拷贝构造器或者拷贝工厂可以带一个参数,参数类型是通过该类实现的接口。

例如,按照惯例,所有通用集合实现都提供一个拷贝构造器,它的参数类型是Collection或者Map。基于接口的拷贝构造器和拷贝工厂允许客户选择拷贝的实现类型,而不是强迫客户接受原始的实现类型。例如,假设有一个HashSet,希望把它拷贝成一个TreeSet,clone方法无法提供这样的功能,但用转换构造器实现:new TreeSet(s)。

有些专家级的程序员干脆从来不去覆盖clone方法, 也从来不去调用它,除非拷贝数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值