Java对象拷贝

Java 对象拷贝

Java有两大数据类型:内置数据类型和引用数据类型。对象和数组都是引用数据类型。在Java中,引用类型的变量非常类似于C/C++的指针。既然如此,将一个对象复制给另一个对象,其应该是引用地址的复制,那么改变一个对象的值,另一个对象的值应该也会随之而改变。

让我们来看看:

1
2
3
4
5
6
7
8
9
public class ObjectCopy {
public static void main(String[] args) {
String test = "test";
String temp = test;
temp = "temp";
System.out.println(test);
System.out.println(temp);
}
}

运行结果:

1
2
3
➜  java ObjectCopy
test
temp

咦,为什么test对象没有随着temp对象的值得改变而改变呢?

其实问题出在temp = "temp";, 自己对Java对象的概念还是没有理解的透彻,这里还是将对象的初始化与内置数据类型的赋值的概念混淆在了一起。这里的temp是一个String对象,而temp = "temp"是将"temp"字面量的地址赋值给了temp,并没有改变test指向的对象。

StringCopy

因为String 类是不可改变的,一旦创建了 String 对象,那它的值就无法改变了。所有,让我们换一个对象试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Student {  
private int number;

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = stu1;
stu2.setNumber(123456);

System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}

运行结果:

1
2
学生1:123456  
学生2:123456

可以看到当我们修改了stu2的number值,stu1的number值也发生了变化。

ObjectCopy

如果我们想对一个对象进行拷贝得到对象的一个副本,而不是只是引用的拷贝,应该怎么做呢?那么在这之前,我们有必要了解一下对象的拷贝。

对象拷贝

Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。

在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部数据。

这里要对前面介绍的Object a = new Object(); Object b = a;这种形式的拷贝进行说明,这种对象的拷贝只是将对象的引用进行了拷贝,a和b仍然指向的是同一个对象。

Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。

浅拷贝

  • 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据.
  • 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
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
class Age {
private int age;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
class Student {
private int number;
private Age age;

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

private int getAge() {
return age.getAge();
}

public void setAge(int age) {
age.setAge(age);
}
}

SCopy

浅拷贝的实现方式主要有两种:

  • 通过拷贝构造函数实现浅拷贝;
  • 通过重写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
class Age {
private int age;

public Age(int age) {
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

class Student {
private int number;
private Age age;

public Student(int num, Age age) {
this.number = num;
this.age = age;
}
//拷贝构造函数
public Student(Student stu) {
this.number = stu.number;
this.age = stu.age;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public int getAge() {
return age.getAge();
}

public void setAge(int age) {
this.age.setAge(age);
}
}

public class Test {

public static void main(String args[]) {
Age age = new Age(20);
Student stu1 = new Student(1, age);
Student stu2 = new Student(stu1);
System.out.println("修改前,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge());
System.out.println("修改前,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge());
stu1.setNumber(2);
stu1.setAge(25);
System.out.println("修改后,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge());
System.out.println("修改后,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge());
}
}

运行结果:

1
2
3
4
5
➜ java Test
修改前,学生1 number:1, age: 20
修改前,学生2 number:1, age: 20
修改后,学生1 number:2, age: 25
修改后,学生2 number:1, age: 25

可以看出,Student类有2个成员变量,一个是内置数据类型number,一个是引用数据类型age。stu2是通过拷贝构造函数对stu1进行拷贝生成的对象。当修改stu1的number值和age对象时,stu2的number值并没有随着改变,而stu2的age对象随之改变。

当然,如果在拷贝构造方法中,对引用数据类型变量不是简单的赋值,而是开辟新的内存空间,创建新的对象,也可以实现深拷贝。而对于一般的拷贝构造,则一定是浅拷贝。

通过重写clone()方法进行浅拷贝

Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。

但是需要注意:

  1. Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。
  2. 使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException

示例:

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
class Age {
private int age;

public Age(int age) {
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

class Student implements Cloneable{
private int number;
private Age age;

public Student(int num, Age age) {
this.number = num;
this.age = age;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public int getAge() {
return age.getAge();
}

public void setAge(int age) {
this.age.setAge(age);
}

public Object clone() {
Student stu = null;
try {
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}

}
public class Test {

public static void main(String args[]) {
Age age = new Age(20);
Student stu1 = new Student(1, age);
Student stu2 = (Student)stu1.clone();
System.out.println("修改前,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge());
System.out.println("修改前,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge());
stu1.setNumber(2);
stu1.setAge(25);
System.out.println("修改后,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge());
System.out.println("修改后,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge());
}
}

运行结果:

1
2
3
4
5
➜ java Test
修改前,学生1 number:1, age: 20
修改前,学生2 number:1, age: 20
修改后,学生1 number:2, age: 25
修改后,学生2 number:1, age: 25

可以得到和拷贝构造函数一样的结果。

深拷贝

首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。

那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!

DCopy

深拷贝的实现方法主要有三种:

  • 通过拷贝构造函数实现深拷贝;
  • 通过重写clone()方法进行深拷贝:
  • 通过对象序列化实现深拷贝;

通过拷贝构造函数实现深拷贝

在拷贝构造方法中,对引用数据类型变量不是简单的赋值,而是开辟新的内存空间,创建新的对象,也可以实现深拷贝。

让我们对浅拷贝中的构造函数进行修改:

1
2
3
4
public Student(Student stu) {
this.number = stu.number;
this.age = new Age(stu.age.getAge());
}

让我们再运行:

1
2
3
4
5
➜ java Test
修改前,学生1 number:1, age: 20
修改前,学生2 number:1, age: 20
修改后,学生1 number:2, age: 25
修改后,学生2 number:1, age: 20

可以看到引用类型成员变量并没有随着发生改变。

通过重写clone()方法进行深拷贝

与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的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
class Age implements Cloneable {
private int age;

public Age(int age) {
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Age clone() {
Age age = null;
try {
age = (Age)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return age;
}
}

class Student implements Cloneable{
private int number;
private Age age;

public Student(int num, Age age) {
this.number = num;
this.age = age;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public Age getAge() {
return this.age;
}

public void setAge(int age) {
this.age.setAge(age);
}

public Object clone() {
Student stu = null;
try {
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
stu.age = (Age)stu.getAge().clone();
return stu;
}

}
public class Test {

public static void main(String args[]) {
Age age = new Age(20);
Student stu1 = new Student(1, age);
Student stu2 = (Student)stu1.clone();
System.out.println("修改前,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge().getAge());
System.out.println("修改前,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge().getAge());
stu1.setNumber(2);
stu1.setAge(25);
System.out.println("修改后,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge().getAge());
System.out.println("修改后,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge().getAge());
}
}

运行结果:

1
2
3
4
5
➜ java Test
修改前,学生1 number:1, age: 20
修改前,学生2 number:1, age: 20
修改后,学生1 number:2, age: 25
修改后,学生2 number:1, age: 20

通过对象序列化实现深拷贝

虽然上述两种方法能够实现深拷贝,但是我给出的例子都是十分简单的。如果一个类有个多层对象图,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象。我们得为每个对象实现构造函数,开辟新的内存空间或者为每个函数重写clone函数,然后在最顶层的类的重写的clone方法中调用所有的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
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Age implements Serializable {
private int age;

public Age(int age) {
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

class Student implements Serializable {
private int number;
private Age age;

public Student(int num, Age age) {
this.number = num;
this.age = age;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public Age getAge() {
return this.age;
}

public void setAge(int age) {
this.age.setAge(age);
}
}

public class Test {
public static void main(String args[]) throws IOException, ClassNotFoundException {
Age age = new Age(20);
Student stu1 = new Student(1, age);
//通过序列化方法实现深拷贝
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(stu1);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Student stu2=(Student)ois.readObject();

System.out.println("修改前,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge().getAge());
System.out.println("修改前,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge().getAge());
stu1.setNumber(2);
stu1.setAge(25);
System.out.println("修改后,学生1 number:" + stu1.getNumber() + ", age: " + stu1.getAge().getAge());
System.out.println("修改后,学生2 number:" + stu2.getNumber() + ", age: " + stu2.getAge().getAge());
}
}

运行结果:

1
2
3
4
5
➜ java Test
修改前,学生1 number:1, age: 20
修改前,学生2 number:1, age: 20
修改后,学生1 number:2, age: 25
修改后,学生2 number:1, age: 20

可以通过相对简洁的代码实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。