写一个简短的开场白吧,最近在深入一些框架的学习,但是感觉有些吃力,甚至怀疑自己到底适不适合当程序员。总而言之,过程很痛苦,用别人造好的轮子感觉爽的飞起,一旦研究别人的轮子,瞬间被秒。既然想成为大神,想成为一个设计轮子的人,这些是必须经历的吧。在获得什么之前,总的付出些什么吧。一句话,死磕到底。
今天第一个关于Java的问题是,对象克隆。
对象克隆
/**
* @ClassName: Student
* @Description: TODO
* @Author: kevin
* @Date: 2019-04-07 01:39
* @Version: 1.0
**/
public class Student implements Cloneable {
private String name;
private Integer age;
private StringBuilder birthday;
public Student(String name, Integer age, StringBuilder birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public StringBuilder getBirthday() {
return birthday;
}
public void setBirthday(StringBuilder birthday) {
this.birthday = birthday;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}
}
/**
* @ClassName: CloneTest
* @Description: TODO
* @Author: kevin
* @Date: 2019-04-07 01:41
* @Version: 1.0
**/
public class CloneTest {
public static void main(String[] args) {
Student S1 = new Student("A", 18, new Date());
Student S2 = S1;
System.out.println(S1);
System.out.println(S2);
}
}
首先,看完上述内容,可能第一反应就是,这是毛线的克隆。其实这的确不是克隆,这仅仅是一个简单的对象的引用,S1 和 S2 都指向同一个 Student 对象。因此,这两个实例对象中的任何一个修改 Student 中的内容,那么另一个的实例所引用的属性也会发生变化。因为,在内存中本就只有一个 new 出来的Student。
接下来,该克隆了。clone() 方法是Object中的一个protected方法,因此,想要调用 clone() 方法是有前提条件的(这是由protected决定的,因为clone() 方法比较复杂,在设计之初,就为了防止误用,因此加上protected),protected对本包的所有子类可见,很明显,上述中的 Student 类和 clone() 方法不在一同个包中,因此,想要使用它,必须实现 Cloneable 接口,并重写clone()方法。
Cloneable 接口的出现与接口的正常使用没有关系。该接口没有指定clone()方法,这个接口只是作为一个标记,指示类设计者了解克隆过程。(《Java核心技术 卷1》)
/**
* @ClassName: CloneTest
* @Description: TODO
* @Author: kevin
* @Date: 2019-04-07 01:41
* @Version: 1.0
**/
public class CloneTest {
public static void main(String[] args) {
Student student1 = new Student("A", 18, new StringBuilder("1995"));
try {
Student student2 = (Student) student1.clone();
System.out.println(student1);
System.out.println(student2);
student2.setName("B");
student2.setBirthday(student2.getBirthday().append("09"));
System.out.println(student1);
System.out.println(student2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
================
结果
================
Student{name='A', age=18, birthday=1995}
Student{name='A', age=18, birthday=1995}
Student{name='A', age=18, birthday=199509}
Student{name='B', age=18, birthday=199509}
问题来了,当执行 Student student2 = (Student) student1.clone(); 时,的确 new出新的 Student 对象实例(新开辟了一块区域),并且实例对象的初始状态和之前的保持一致。但是在更改 student2 时,发现连同 student1 的生日一起变了。
图中 Date 类应该为 StringBuilder 类,这两个类都是可变的类。但是,为了方便操作使用 StringBuilder 类
上图就是产生问题的原因,因为 name 和 birthday 共用了一个存储区域(也就是浅拷贝),导致 student2 修改的同时,将 student1 的生日一起修改了。但是这里有个问题,为什么名字没有被一起修改?
- 浅拷贝,如果原对象和克隆对象共享的子对象是不可变的,那么这种克隆就是安全的。
- 本例中,共享子对象有String,StringBuilder,这两个类,其中 String 是被 final 修饰的类,也就是不可变的类,而 StringBuilder 类不是不可变的类,因此在克隆 birthday 时就出现了问题。
- 总结,基本数据类型,不可变类,在浅拷贝时,不会出现任何问题。
原因已经知晓,该怎么解决呢,代码如下
/**
* @ClassName: Student
* @Description: TODO
* @Author: kevin
* @Date: 2019-04-07 01:39
* @Version: 1.0
**/
public class StudentA implements Cloneable {
private String name;
private Integer age;
private StringBuilder birthday;;
public StudentA(String name, Integer age, StringBuilder birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public StringBuilder getBirthday() {
return birthday;
}
public void setBirthday(StringBuilder birthday) {
this.birthday = birthday;
}
@Override
protected StudentA clone() throws CloneNotSupportedException {
StudentA studentA = (StudentA) super.clone();
studentA.birthday = new StringBuilder(birthday);
return studentA;
}
@Override
public String toString() {
return "StudentA{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}
}
注意,在重写 clone() 方法时,把可变类 StringBulider 也进行了一次新的创建,并且初始值仍为克隆前的值,这样就能解决浅拷贝的问题。
实验对比
/**
* @ClassName: CloneTest
* @Description: TODO
* @Author: kevin
* @Date: 2019-04-07 01:41
* @Version: 1.0
**/
public class CloneTest {
public static void main(String[] args) {
Student student1 = new Student("A", 18, new StringBuilder("1995"));
try {
Student student2 = (Student) student1.clone();
System.out.println(student1);
System.out.println(student2);
student2.setName("B");
student2.setBirthday(student2.getBirthday().append("09"));
System.out.println(student1);
System.out.println(student2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println("--------------------");
StudentA studentA1 = new StudentA("A", 18, new StringBuilder("1995"));
try {
StudentA studentA2 = studentA1.clone();
System.out.println(studentA1);
System.out.println(studentA2);
studentA2.setName("B");
studentA2.setBirthday(studentA2.getBirthday().append("09"));
System.out.println(studentA1);
System.out.println(studentA2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
================
结果
================
Student{name='A', age=18, birthday=1995}
Student{name='A', age=18, birthday=1995}
Student{name='A', age=18, birthday=199509}
Student{name='B', age=18, birthday=199509}
--------------------
StudentA{name='A', age=18, birthday=1995}
StudentA{name='A', age=18, birthday=1995}
StudentA{name='A', age=18, birthday=1995}
StudentA{name='B', age=18, birthday=199509}
参考资料
《Java核心技术 卷1》