为什么要使用对象克隆

首先需要说明的是,直接用=符号来复制,只是复制了原有对象的引用,对新对象的修改同样也会作用原对象上。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A {
private String name;

public A(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public class CloneTest {
public static void main(String[] args) {
A a = new A("张三");
A b = a;
System.out.println(a.getName()); // 张三
b.setName("李四");
System.out.println(a.getName()); // 李四
System.out.println(b.getName()); // 李四
}
}

如上,通过等于符号来赋值只是将对象的引用地址复制了一份。所以想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了,Java 语言中克隆针对的是类的实例。

实现

对象克隆的实现一共有三种方式,一种浅拷贝,两种深度克隆。

实现 Cloneable 接口并重写 Object 类中的 clone()方法(浅拷贝)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* Person类
*/
class Person implements Cloneable {
private String name; // 姓名
private int age; // 年龄
private Car car; // 座驾

public Person(String name, int age, Car car) {
this.name = name;
this.age = age;
this.car = car;
}

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 Car getCar() {
return car;
}

public void setCar(Car car) {
this.car = car;
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}


/**
* 小汽车类
*/
class Car implements Cloneable {
private String brand; // 品牌
private int maxSpeed; // 最高时速

public Car(String brand, int maxSpeed) {
this.brand = brand;
this.maxSpeed = maxSpeed;
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getMaxSpeed() {
return maxSpeed;
}

public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

@Override
public String toString() {
return "Car [brand=" + brand + ", maxSpeed = " + maxSpeed + "]";
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Car c1 = new Car("bc", 100);
Person p1 = new Person("a", 1, c1);
Person p2 = (Person) p1.clone();
System.out.println(p2);
// Person [name=a, age=1, car=Car [brand=bc, maxSpeed = 100]]
p2.setName("b");
p2.getCar().setBrand("bm");
System.out.println(p2);
// Person [name=b, age=1, car=Car [brand=bm, maxSpeed = 100]]
System.out.println(p1);
// Person [name=a, age=1, car=Car [brand=bm, maxSpeed = 100]]
// 浅拷贝,只拷贝引用地址
}

}

为什么是浅克隆?当对象的属性为其他对象时,会出现一个问题:只复制了属性的引用,而没有拷贝属性的值。

clone 对象时也 clone 属性,实现深度克隆

缺点是自定义引用过多时,将增加代码复杂度。

clone 方法示例如下,其余跟浅克隆一样:

1
2
3
4
5
6
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setCar((Car) person.getCar().clone());
return person;
}

实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 序列化工具
class MyUtil {

private MyUtil() {
throw new AssertionError();
}

@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj)
throws Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);

ByteArrayInputStream bin =
new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (T) ois.readObject();

// 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
}
}

/**
* Person类
*/
class Person implements Serializable {

private String name; // 姓名
private int age; // 年龄
private Car car; // 座驾

public Person(String name, int age, Car car) {
this.name = name;
this.age = age;
this.car = car;
}

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 Car getCar() {
return car;
}

public void setCar(Car car) {
this.car = car;
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
}
}


/**
* 小汽车类
*/
class Car implements Serializable {
private String brand; // 品牌
private int maxSpeed; // 最高时速

public Car(String brand, int maxSpeed) {
this.brand = brand;
this.maxSpeed = maxSpeed;
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getMaxSpeed() {
return maxSpeed;
}

public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

@Override
public String toString() {
return "Car [brand=" + brand + ", maxSpeed = " + maxSpeed + "]";
}
}

public class CloneTest {
public static void main(String[] args) throws Exception {
Car c1 = new Car("bc", 100);
Person p1 = new Person("a", 1, c1);
Person p2 = MyUtil.clone(p1);
System.out.println(p2);
// Person [name=a, age=1, car=Car [brand=bc, maxSpeed = 100]]
p2.setName("b");
p2.getCar().setBrand("bm");
System.out.println(p2);
// Person [name=b, age=1, car=Car [brand=bm, maxSpeed = 100]]
System.out.println(p1);
// Person [name=a, age=1, car=Car [brand=bc, maxSpeed = 100]]
}

}

注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

浅拷贝和深拷贝的区别

  • 浅拷贝对于对象属性来说只是复制了对象属性的引用地址,两个不同对象的同一属性依然指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign ())
  • 深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse () 和 JSON.stringify (),但是此方法无法复制函数类型)