Java基础 —— 对象克隆(clone)详解

时间:2024-03-18 18:02:42

在平时写代码的过程中,有时候我们希望能把当前对象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同样加了一天,这是我们不希望看到的。那这是为什么呢?我们先来看看上面的赋值语句到底做了什么。

Java基础 —— 对象克隆(clone)详解

如上图,其实赋值语句 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方法做了什么。

Java基础 —— 对象克隆(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接口只是作为一个标记,指示类设计者了解克隆过程。

以上为个人学习总结,如有问题欢迎文明吐槽。

相关文章