浅拷贝

概述:

  1. 对于数据类型是基本数据类型和String的成员变量,浅拷贝会直接进行值传递。即对此类型的成员变量值进行修改,不会影响另一个对象拷贝得到的数据
  2. 对于数据类型是 引用数据类型 的成员变量,浅拷贝会进行引用传递。即在一个对象中修改成员变量会影响到另一个对象的成员变量值

如果不进行特殊处理,引用数据类型的拷贝都是浅拷贝

浅拷贝的实现:

以前记得笔记,可以不看,看了反而会晕

例程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 若用clone(),还要给进行拷贝的类实现Cloneable接口
public class Student /* implements Cloneable */{
String id;
Age age;
public Student(){

}
public Student(Student student){
this.id = student.id;
this.age = student.age;
}
public Student(String id,Age age){
this.id = id;
this.age = age;
}

/* 若用 clone() 还需要重写 clone() 方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); //引用父类的clone()
}
*/
}
1
2
3
4
5
6
7
8
9
10
11
public class Age {
int ageNum;

public Age() {

}

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

普通方法实现浅拷贝

通过赋值的方式实现浅拷贝。(构造方法浅拷贝实际上也是一种赋值,只不过其赋值是在类的构造函数中实现的)

1
2
3
4
5
6
7
8
9
Student s1 = new Student("12",new Age(20));
Student s2 = new Student(s1);
Student s3 = s1;

s2.id = "21";s2.age.ageNum = 10;
// s1.id = 12,s1.age.ageNum = 10;
s3.id = "21";s3.age.ageNum = 30;
// s1.id = 21,s1.age.ageNum = 30;

为什么呢?

  1. s3是直接由s1赋值得到的,整个s3都是对s1的浅拷贝
  2. s2是通过构造函数得到的,而Student的构造函数是分别对idage进行赋值,其中age是引用数据类型。所以s2是对s1的age进行了浅拷贝。

通过重写clone()方法实现浅拷贝

只对当前类中的引用数据类型进行浅拷贝,类似于普通浅拷贝中的构造函数拷贝方式。
当然,只局限于当前类,也就是说,Student -> age -> ageNum 虽是基本数据类型,但由于在Student中的是age不是ageNum,所以只会对age进行浅拷贝。

快捷实现:

  1. 给要拷贝的类实现Cloneable接口
  2. 在该类重写clone(),clone()里面调用父类 — Object 的clone()。在IDEA中可以通过快捷键alt+ins直接生成
1
2
3
4
5
6
7
8
9
Student s1 = new Student("12",new Age(20));
Student s2 = null;
try {
s2 = (Student) s1.clone();//返回值是Object,需要强转
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
s2.id = "21";s2.age.ageNum = 10;
// s1.id = "123",s1.age.ageNum = 10;

在Student中,id是int,值传递,不变;age是Age,引用传递,变);

深拷贝

概述

  1. 简单地说,深拷贝对引用数据类型的成员变量中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建新的内存空间。
  2. 深拷贝和浅拷贝的区别体现在引用数据类型上
  3. 经过深拷贝后,引用数据类型的值不会随拷贝的值的变化而变化,相互独立

实现深拷贝

通过多次重写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
// 实现Cloneable接口
public class Student implements Cloneable {
String id;
Age age; //引用数据类型

public Student() {

}

public Student(Student student) {
this.id = student.id;
this.age = student.age;
}

public Student(String id, Age age) {
this.id = id;
this.age = age;
}

// 在最顶层的类重写的clone()方法中 调用其父类的clone()方法和其内层的所有clone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone(); //调用 自己父类 的clone()方法实现一次浅拷贝
Student s = (Student) obj;
s.age = (Age) s.age.clone(); //调用内层Age类的clone()方法实现一次浅拷贝
s.age.birthdayYear = (BirthdayYear) s.age.birthdayYear.clone(); //调用内层 BirthdayYear类的clone()方法实现一次浅拷贝
return obj;// 进行 多层浅拷贝 最终得到的obj对象,实现了深拷贝
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现Cloneable接口、
public class Age implements Cloneable {
int ageNum;
BirthdayYear birthdayYear; //引用数据类型

public Age() {

}

public Age(int age, BirthdayYear birthdayYear) {
this.ageNum = age;
this.birthdayYear = birthdayYear;
}

@Override
// 重写Object 的 clone()方法
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 实现Cloneable接口
public class BirthdayYear implements Cloneable {
int year;

public BirthdayYear() {

}

public BirthdayYear(int year) {
this.year = year;
}

@Override
// 重写Object 的 clone()方法
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

具体步骤

  1. 每一层 每一个引用数据对象都使用浅拷贝以实现深拷贝

    • 每个类都实现Cloneable接口
    • 每个类都重写clone()方法。除最顶层类外,都返回父类的clone(),即 super.clone()
  2. 在最顶层类重写的clone()方法中 调用其内层的所有clone()方法

总结以上两点,可以得到此方法实现思路:
深拷贝=对每一层 每一个引用数据对象都进行一次浅拷贝

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("21", new Age(20, new BirthdayYear(2000)));
Student s2 = (Student) s1.clone();// s1进行深拷贝给s2(深拷的过程再具体类里实现)

s2.id = 12;
s2.age.ageNum = 10;
s2.age.birthdayYear.year = 2020;//改变s2中的量
// 此时s1的值仍是 [21,20,2000],深拷贝实现成功
}

由此可以看出,经过深拷贝后s1中引用数据类型的值不会随s2的值的变化而变化

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

为什么要用序列化实现深拷贝?

虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

总而言之:一层一层的实现浅拷贝,对应层级关系小的类还好说,但是一旦层级关系深的话,就太累啦!

关于Java序列化,可以参考以下文章
https://www.cnblogs.com/9dragon/p/10901448.html
https://www.jianshu.com/p/bee1c29c366e

例程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实现序列化接口 Serializable
public class Student implements Serializable {

String name;
Age age;
int length;

public Student(String name, Age age, int length) {
this.name = name;
this.age = age;
this.length = length;
}

//设置输出的字符串形式
public String toString() {
return "姓名是: " + name + ", 年龄为: " + age.toString() + ", 高度是: " + length;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 实现序列化接口 Serializable
public class Age implements Serializable {
int ageNum;

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

//设置输出的字符串形式
public String toString() {
return ageNum +"";
}
}

具体步骤

  1. 将每一个引用数据类型都实现Serializable接口
  2. 具体实现可以调用自拟的Object DeepCopyBySerializable(T t) 方法
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
  //通过序列化方法实现深拷贝
public static <T> Object DeepCopyBySerializable(T t) {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
Object obj = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
oos = new ObjectOutputStream(bos);
oos.writeObject(t);
oos.flush();
ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
obj = ois.readObject();
//关闭流
oos.close();
bos.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return obj;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Student s1 = new Student("Jack", new Age(20), 175);

//通过序列化和IO流,将s1的值深拷贝给s2
Student s2 = (Student) DeepCopyBySerializable(s1);

//尝试修改s2中的各属性,观察s1的属性会不会发生变化
s2.name = "大傻子";
s2.length = 185;
s2.age.ageNum = 25;

//此时s1的值仍为[Jack,20,175],并没有随之改变
}

可以看出,成功实现了深拷贝。
这是因为此深拷贝由序列化和反序列化实现,具体改变是对字符串进行操作的。

2种方法的区别

  1. 多次clone():可读性强,但繁琐;序列化:可读性差,但简洁
  2. 多次clone():主要操作在需要被拷贝的类里;序列化:主要操作在main方法或自拟方法中

小结

上次对Java深拷贝的学习只是匆匆扫过,知道个大概。今天重新将其整理一遍,在对知识点复习的同时,也算让我对其的理解更深刻了吧,对我当时笔记中的一些不合理部分也进行了删除和更改。深拷贝的知识以目前的我来说还不是很用的到,希望下次的复习能让我更有收获和感悟。