在java中我们创建对象可以通过new的方式创建,但是除了这种最常见的方式外,我们也可以通过反射、clone方法和反序列化的方式来创建对象。本文主要讲解clone方法和序列化这两种。
使用clone方法需要实现Cloneable接口和覆写clone方法。需要注意的是:用clone方法创建对象并不会调用构造器。
public class Person implements Cloneable {
private String name;
private int age;
private Phone phone;
public Person(String name, int age, Phone phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
public Person() {}
private class Phone {
private String type;
private Phone(String type) {
this.type = type;
}
private String getType() {
return type;
}
private void setType(String type) {
this.type = type;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
@Override
public Person clone() {
Person person = null;
try {
person = (Person) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return person;
}
public static void main(String[] args) {
Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
Person cloneMan = man.clone();
System.out.println("man: " + man);
System.out.println("cloneMan: " + cloneMan);
System.out.println("man == cloneMan: " + (man == cloneMan));
System.out.println("man.getPhone() == cloneMan.getPhone(): " + (man.getPhone() == cloneMan.getPhone()));
}
}
如上所示:覆写clone方法时可以将访问限制符改为public,这样可以确保任何类都能调用clone方法。运行结果如下:
man: com.hys.test.Person@7852e922
cloneMan: com.hys.test.Person@4e25154f
man == cloneMan: false
man.getPhone() == cloneMan.getPhone(): true
可以看到,两个对象是存在不同的内存地址中,所以是false。但是从最后一行结果可知:Person类内的Phone却引用了同一个对象。所以如果类中的属性是引用类型,则引用类型的拷贝是拷贝的引用,而不是值。即clone方法是浅拷贝的,这点需要特别注意。深拷贝和浅拷贝的区别如下图所示(String常量池在jdk7之前是存在永久代(PermGen)中,但从jdk7开始进行去永久代的工作,常量池也随即放在了堆中,在jdk8中完全去掉了永久代的概念。详情可以查看笔者的另一篇博客《JVM总结》):
那么浅拷贝会带来什么问题呢?考虑下面的代码:
Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
Person cloneMan = man.clone();
man.getPhone().setType("iPhone X");
System.out.println("man.getPhone().getType(): " + man.getPhone().getType());
System.out.println("cloneMan.getPhone().getType(): " + cloneMan.getPhone().getType());
运行结果:
man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone X
当我去修改原对象的引用对象属性值的时候,发现克隆对象的值也跟着一起被修改了。这就是引用了同一个对象所导致的问题,并没有做到完全的隔离。
如果想要做到完全的隔离,也就是深拷贝,需要将Phone内部类也实现Cloneable接口和覆写clone方法,Phone内部类修改如下:
private class Phone implements Cloneable {
private String type;
private Phone(String type) {
this.type = type;
}
private String getType() {
return type;
}
private void setType(String type) {
this.type = type;
}
@Override
public Phone clone() {
Phone head = null;
try {
head = (Phone) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return head;
}
}
同时,在Person类的clone方法里也要调用Phone的clone方法,如下:
@Override
public Person clone() {
Person person = null;
try {
person = (Person) super.clone();
person.phone = (Phone) phone.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return person;
}
此时再次运行测试方法,得到正确的结果:
man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone 8 Plus
这种方式虽然能实现深拷贝,但是对于类中内部属性来说,都要判断是否是引用类型,然后实现Cloneable接口和覆写clone方法。这对于简单的类属性还好,但是如果类中加入了很多复杂的内部类属性,内部类中又有引用类型,这种方式显然不推荐使用。
所以如果对于复杂对象的情况下,我们可以考虑使用序列化和反序列化的方式来进行克隆。序列化需要实现Serializable接口,它能做到真正的深拷贝。代码如下:
public class Person implements Serializable {
private static final long serialVersionUID = 5093838173041282334L;
private String name;
private int age;
private Phone phone;
public Person(String name, int age, Phone phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
public Person() {}
private class Phone implements Serializable {
private static final long serialVersionUID = 6683221508874474120L;
private String type;
private Phone(String type) {
this.type = type;
}
private String getType() {
return type;
}
private void setType(String type) {
this.type = type;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
public Person clone(Person obj) {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (Person) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
Person cloneMan = man.clone(man);
System.out.println("man: " + man);
System.out.println("cloneMan: " + cloneMan);
System.out.println("man == cloneMan: " + (man == cloneMan));
System.out.println("man.getPhone() == cloneMan.getPhone(): " + (man.getPhone() == cloneMan.getPhone()));
man.getPhone().setType("iPhone X");
System.out.println("man.getPhone().getType(): " + man.getPhone().getType());
System.out.println("cloneMan.getPhone().getType(): " + cloneMan.getPhone().getType());
}
}
需要说明的是:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义。这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放。运行结果如下:
man: com.hys.test.Person@42a57993
cloneMan: com.hys.test.Person@7cca494b
man == cloneMan: false
man.getPhone() == cloneMan.getPhone(): false
man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone 8 Plus