在平时写代码的过程中,有时候我们希望能把当前对象copy一份,赋值给一个新的变量,并且这两个变量能够互不影响。
让我们先来看下面的代码:
// User[id, name, age, birthday]
User curr = new User(1, "张三", 111, new Date()); // 当前对象
System.out.println(formatDate(curr.getBirthday())); // 2019-03-24
User copy = curr;
copy.setBirthday(new Date((new Date().getTime() + 24 * 60 * 60 * 1000))); // 加一天
System.out.println("curr: " + formatDate(curr.getBirthday())); // 2019-03-25
System.out.println("copy: " + formatDate(copy.getBirthday())); // 2019-03-25
如上面的代码,我们创建了一个User实例并把它赋值给 curr 变量,接着我们把 curr 赋值给 copy,然后把 copy 的birthday加一天。我们虽然成功地把copy的birthday加了一天,但是也把curr的birthday同样加了一天,这是我们不希望看到的。那这是为什么呢?我们先来看看上面的赋值语句到底做了什么。
如上图,其实赋值语句 copy = curr,并不是把对象curr赋值给copy,而是直接把对象的引用赋值给了copy变量,至此 curr 和 copy 是指向的是同一个对象的引用,所以只要其中一个变量进行改变,另一个变量也跟着改变,没有达到我们想要的效果。
那我们就没有办法了吗?答案当然是否定的。Java给我们提供了一个clone方法来进行对象克隆,用法如下:
(1)需要进行克隆的对象必须实现Cloneable接口;
(2)需要重新定义 clone 方法,且该方法需要用 public 来修饰;
具体如下:
public class User implements Cloneable{
private int id;
private String name;
private int age;
private Address addr;
public User(int id, String name, int age, Address addr) {
super();
this.id = id;
this.name = name;
this.age = age;
this.addr = addr;
}
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
...
}
在上面代码中,User类实现了Cloneable接口,并实现了public clone() 方法,clone方法中调用的是 Object 默认的clone方法,我们来看看测试结果会不会达到我们的预期呢?
Address addr = new Address("jiangsu");
// User[id, name, age, address]
User curr = new User(1, "张三", 111, addr);
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=jiangsu]]
User copy;
try {
copy = (User) curr.clone();
copy.setAge(copy.getAge() + 1); // 加一岁
<!-- insert -->
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=jiangsu]]
System.out.println("copy: " + copy.toString()); // [id=1, name=张三, age=112, addr=[province=jiangsu]]
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
看样子好像达到了我们想要的效果,But,如果我们在上面的insert处加上一句呢? 如下:
copy.getAddr().setProvince("zhejiang");
再打印一下看看:
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=zhejiang]]
System.out.println("copy: " + copy.toString()); // [id=1, name=张三, age=112, addr=[province=zhejiang]]
可以看到,我只是把copy的addr改变了一下,为什么curr 的addr也跟着变了呢?让我们来看看 Object 默认的clone方法做了什么。
由上图可知,clone 方法确实复制了一个副本对象赋值到copy变量,但对象中的 addr(引用类型)却是公用的,只是实现了 “浅拷贝”。所谓浅拷贝,指的是只能对八种基本类型进行完全的克隆,而对于类似于 Address 的引用类型却不能克隆,这也是 addr 同时改变的根本原因。所以,浅拷贝会导致原对象和浅克隆对象共享对象内的子对象,这是不安全的。当然,也不是所有的引用类型在浅拷贝共享的时候都是不安全的,像一些不可变的类(比如:String)或者 子对象在整个生命期中一直包含不变的常量,且无法改变时,是安全的。举个栗子:
copy.setName(copy.getName() + 1);
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=zhejiang]]
System.out.println("copy: " + copy.toString()); // [id=1, name=张三1, age=112, addr=[province=zhejiang]]
String类型的 name 在改变时,curr 的 name 并没有跟着改变。
既然浅拷贝无法达到我们想要的结果,那就试试“深拷贝”,其实也很简单,就是自定义clone方法:
@Override
public Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.addr = (Address) this.addr.clone();
return user;
}
注意:因为addr也用到了clone,所以 Address 也必须实现 Cloneable 接口。
再看看效果:
Address addr = new Address("jiangsu");
// User[id, name, age, address]
User curr = new User(1, "张三", 111, addr);
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=jiangsu]]
User copy;
try {
copy = (User) curr.clone();
copy.setAge(copy.getAge() + 1); // 加一岁
copy.getAddr().setProvince("zhejiang");
copy.setName(copy.getName() + 1);
System.out.println("curr: " + curr.toString()); // [id=1, name=张三, age=111, addr=[province=jiangsu]]
System.out.println("copy: " + copy.toString()); // [id=1, name=张三1, age=112,addr=[province=zhejiang]]
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
果然达到我们想要的效果,两个对象互不影响。
总结:在克隆(clone)之前我们需要确定:
(1)默认的clone方法是否满足要求;
(2)是否可以在可变的子对象上调用clone来修补默认的 clone 方法;
(3)是否不该时候 clone。
如果确定要使用 clone,则需要做到以下两点:
(1)实现 Cloneable 接口;
(2)重新定义 clone 方法,并指定 public 访问修饰符。
知识点:Cloneable 接口
Cloneable接口值Java提供的一组标记接口之一,它不包含任何方法。所以, Cloneable 接口的出现与接口的正常使用并没有关系。具体来说,它没有指定clone方法,这个方法是从 Object 类继承的。Cloneable接口只是作为一个标记,指示类设计者了解克隆过程。
以上为个人学习总结,如有问题欢迎文明吐槽。