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方法, 也从来不去调用它,除非拷贝数组。