一、先看jdk中对Cloneable接口和clone方法的解释
Cloneable接口
- 一个类声明实现了Cloneable接口,就是表示Object中实现域值复制的clone()方法可以合法调用。假如一个类在没声明实现Cloneable接口的情况下,直接调用clone()方法,将会抛出CloneNotSupportedException的异常。
- Cloneable接口中没有任何方法,但是我们一般都会重写clone()方法,并且将它的访问权限设置为public,否则我们无法直接调用clone方法,因为Object.clone()方法是protected。
Object.clone()方法
- 该方法用于创建和返回一个对象的复制。它规定了以下非常弱的约定,但是这并不是绝对的要求:
- x.clone() != x 是true;
- x.clone().getClass() == x.getClass()是true;
- x.clone().equals(x) 是true。
- 它约定,重写clone()类时,应该先调用super.clone()方法。只有类和它的所有父类都遵守了这个约定,才能保证x.clone().getClass() == x.getClass();(但其实我们没办法保证)
- 通常来说,调用clone()方法复制出来的实例应该和调用方保持独立。但是由于实例中可能有某些域是非基本类型(此时只是单纯的复制了引用,即浅复制),所以为了实现深复制,我们一般都会调用super.clone()方法后,不是马上返回,而是对某些域根据实际情况进行修改。
- 注意,所有类型的数组都默认实现了cloneable接口。调用数组的clone方法,会返回克隆的数组(如果是自定义类型数组,clone只是复制了引用)
二、Cloneable接口和clone()方法的使用
根据jdk文档说明,我们知道:
- 要调用clone()方法,必须声明实现cloneable接口,并且重写clone()方法,将方法访问权限从protected改为public
- 重写的clone方法,必须先调用super.clone()。然后修正任何需要修正的域(为了实现深复制)
- clone方法对于自定义类型是浅复制,如果需要实现深复制,需要递归实现自定义类型域的clone方法。
代码如下:
public class Person implements Cloneable{
private Address address;
private Company company;
private Boolean male;
private String name;
public Address getAddress() {
return address;
}
public Company getCompany() {
return company;
}
public Boolean getMale() {
return male;
}
public String getName() {
return name;
}
public void setCompany(Company company) {
this.company = company;
}
public Person(Address address, Company company, Boolean male, String name) {
this.address = address;
this.company = company;
this.male = male;
this.name = name;
}
public static class Address{
private String province;
public Address(String province) {
this.province = province;
}
}
public static class Company implements Cloneable{
private String name;
public Company(String name) {
this.name = name;
}
@Override
public Company clone() throws CloneNotSupportedException {
return (Company)super.clone();
}
}
@Override
public Person clone() throws CloneNotSupportedException {
Person person = (Person)super.clone();
// 实现深复制
person.setCompany(person.getCompany().clone());
return person;
}
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("testProvince");
Company company = new Company("testCompanyName");
Person person = new Person(address, company, true, "test");
Person copiedPerson = person.clone();
// 结果为true,说明clone是域值复制
System.out.println(person != copiedPerson);
// 结果为true,说明对于自定义类型的域,只是复制引用,是浅复制
System.out.println(person.getAddress() == copiedPerson.getAddress());
// 结果为true,说明通过递归调用自定义类型的clone方法,实现了深复制
System.out.println(person.getCompany() != copiedPerson.getCompany());
Person[] persons = new Person[1];
persons[0] = person;
// 数组默认实现了clone方法
Person[] copyPersons = persons.clone();
// 结果为true,说明数组的clone方法,只是复制数组每个元素的引用
System.out.println(persons[0] == copyPersons[0]);
}
}
三、一些问题
- 假如类中有自定义类型域,要实现深复制,要求自定义类型也继承cloneable接口,并实现clone()方法。日常开放中,很少会实现cloneable接口,所以常常是需要侵入的修改代码。为了减少对代码的侵入,我们可以先将类序列化,再反序列化,实现深复制的目的。代码如下:
public class Person{
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public static class Address{
private String province;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
}
public static void main(String[] args){
Address address = new Address();
address.setProvince("testProvince");
Person person = new Person();
person.setAddress(address);
Person copiedPerson = JSON.parseObject(JSON.toJSONString(person), Person.class);
// true,说明不是浅复制
System.out.println(person.getAddress() != copiedPerson.getAddress());
// 是testProvince,说明复制结果正确
System.out.println(copiedPerson.getAddress().getProvince());
}
}
- Object.clone()方法返回的是Object,但是我们重写的时候,返回的是具体的类型,而不是Object。这是合法的,也是我们期望的。因为从java1.5以后,出现了“协变返回类型”,也就是覆盖的方法的返回类型可以是被覆盖方法的返回类型的子类了。这样客户端使用时就不需要转化了。这里也体现了一个通则“永远不要让客户端去做任何类库能够替客户完成的事情”。
- 如果为了继承而专门设计的类,覆盖了clone方法,那么为了它的子类可以灵活的选择是否要实现Cloneable接口,我们需要把这个覆盖的clone方法声明为protected,并且抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口,这样对于clone方法,子类仿佛是直接从Object中扩展过来的一样。
- 如果决定用线程安全的类实现Cloneable接口,那么clone方法必须得到很好的同步,Object的clone方法没有同步。
- 对于不可变类,支持对象拷贝并没有意义,因为被拷贝的对象与原始对象并没有实质的不同。
- 不得不说,Cloneable具有太多的问题,有那么多隐含的规定。对象拷贝更好的方法,是提供一个拷贝构造器或拷贝工厂。
拷贝构造器:public Person(Person person);
拷贝工厂: public static Person newInstance(Person person);
这种方式的优点是:- 不会要求我们遵守尚未制定好的文档规范。
- 不会抛出不必要的受检异常,要求客户端处理。
- 不需要进行类型转换。
- 利用这种方式,有时我们通过拷贝,还能将类型转为其他类型。比如Collection和Map就提供了转为其他类型的拷贝构造器和拷贝工厂。比如:你想将HashSet s,拷贝为TreeSet。clone方法无法实现,但是拷贝构造器却很简单:new TreeSet(s);