无论是浅拷贝还是深拷贝,都可以通过 Object 类的 clone() 方法来完成:
/**
* 拷贝
*
* @author qiaohaojie
* @date 2023/3/5 15:58
*/
public class CloneTest {
public static void main(String[] args) throws Exception {
Person person1 = new Person(23, "青花椒");
Person person2 = (Person) person1.clone();
System.out.println("浅拷贝后:");
System.out.println("person1:" + person1); // person1:Person{age=23, name='青花椒'}
System.out.println("person2:" + person2); // person2:Person{age=23, name='青花椒'}
}
}
扒一下 clone()
方法的源码:
// Java 1.8
protected native Object clone() throws CloneNotSupportedException;
// Java 9
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
其中,@HotSpotIntrinsicCandidate
注解是 Java 9 引入的一个注解,被它标记的方法,在 HotSpot 虚拟机中会有一套高效的实现。需要注意的是,clone() 方法同时是一个本地(native)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。
Person 类有两个字段,分别是 age 和 name,然后重写 toString() 方法:
/**
* person类
*
* @author qiaohaojie
* @date 2023/3/5 16:09
*/
@Data
public class Person implements Cloneable {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Cloneable 接口是一个标记接口,接口里没有内容:
public interface Cloneable {
}
它只是一个标记接口,为什么要实现它呢?
标记接口的作用其实就是用来表示某个功能在执行的时候是合法的。
如果一个类没有实现 Cloneable 接口,即使它重写了 clone() 方法了,但它依然是无法调用该方法进行对象克隆的,程序在执行 clone() 方法的时候会抛出 CloneNotSupportedException 异常:
Exception in thread "main" java.lang.CloneNotSupportedException
测试类:
/**
* 浅拷贝测试-基本类型字段
*
* @author qiaohaojie
* @date 2023/3/5 15:58
*/
public class CloneTest {
public static void main(String[] args) throws Exception {
Person person1 = new Person(23, "青花椒");
Person person2 = (Person) person1.clone();
System.out.println("浅拷贝后:");
System.out.println("person1:" + person1); // person1:Person@86accb4e{age=23, name='青花椒'}
System.out.println("person2:" + person2); // person2:Person@6c58a0a6{age=23, name='青花椒'}
person2.setName("程序猿");
person1.swtAge(66);
System.out.println("修改后:");
System.out.println("person1:" + person1); // person1:Person@86accb4e{age=23, name='青花椒'}
System.out.println("person2:" + person2); // person2:Person@6c58a0a6{age=23, name='程序猿'}
}
}
步骤大概是这样的:
从结果可以看出,浅拷贝后 person1 和 person2 引用了不同的对象,但是值是相同的,说明拷贝成功了。之后再修改 person2 的 name 字段时,引用是这样的:
再自定义一个 Student 类,有 score 和 name 两个字段:
/**
* 学生类
*
* @author qiaohaojie
* @date 2023/3/5 16:31
*/
@Data
public class Student implements Cloneable {
private double score;
private String name;
public Student(double score, String name) {
this.score = score;
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"score=" + score +
", name='" + name + '\'' +
'}';
}
}
修改 Person 类,把 Student 类对象当做一个成员变量:
/**
* person类
*
* @author qiaohaojie
* @date 2023/3/5 16:09
*/
@Data
public class Person implements Cloneable {
private int age;
private String name;
private Student student;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
// 浅拷贝
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"age=" + age +
", name='" + name + '\'' +
", student=" + student +
'}';
}
}
测试类:
/**
* 浅拷贝测试-引用类型字段
*
* @author qiaohaojie
* @date 2023/3/5 16:46
*/
public class CloneTest2 {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(23, "青花椒");
Student student1 = new Student(93.2, "张三");
person1.setStudent(student1);
Person person2 = (Person) person1.clone();
System.out.println("浅拷贝后:");
System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
System.out.println("person2:" + person2); // person2:Person@9853e6e6{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
Student student2 = person2.getStudent();
student2.setName("李四");
student2.setScore(88.8);
System.out.println("person2.变更后:");
System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=88.8, name='李四'}}
System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@67e260bd{score=88.8, name='李四'}}
}
}
步骤大概是这样的:
与基本数据类型拷贝不同的是,person2.student 修改后,person1.student 也发生了改变。这是因为字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变:
值得注意的是,浅拷贝克隆的对象中,引用类型的字段指向的是同一个对象,当改变任何一个对象,另外一个对象也会随之改变,除去字符串的特殊性外。
深拷贝和浅拷贝不同的是,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象时,另外一个对象不会随之改变。
Student 类,重写了 clone() 方法,并实现了 Cloneable 接口,为的就是深拷贝时也能够克隆该字段:
/**
* 学生类
*
* @author qiaohaojie
* @date 2023/3/5 16:31
*/
@Data
public class Student implements Cloneable {
private double score;
private String name;
public Student(double score, String name) {
this.score = score;
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"score=" + score +
", name='" + name + '\'' +
'}';
}
}
Person 类,与之前不同的是,clone() 方法中,不再只调用 Object 的 clone() 方法对 Person 进行克隆了,还对 Student 也进行了克隆:
/**
* person类
*
* @author qiaohaojie
* @date 2023/3/5 16:09
*/
@Data
public class Person implements Cloneable {
private int age;
private String name;
private Student student;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
// 深拷贝
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setStudent(student);
person.setStudent((Student) person.getStudent().clone());
return super.clone();
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"age=" + age +
", name='" + name + '\'' +
", student=" + student +
'}';
}
}
测试类:
/**
* 深拷贝测试
*
* @author qiaohaojie
* @date 2023/3/5 17:06
*/
public class CloneTest3 {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(23, "青花椒");
Student student1 = new Student(93.2, "张三");
person1.setStudent(student1);
Person person2 = (Person) person1.clone();
System.out.println("深拷贝后:");
System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@9853e6e6{score=93.2, name='张三'}}
Student student2 = person2.getStudent();
student2.setName("李四");
student2.setScore(88.8);
System.out.println("person2.变更后:");
System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@9853e6e6{score=88.8, name='李四'}}
}
}
在深拷贝中,不只是 person1 和 person2 是不同的对象,它们中的 student1 和 student2 也是不同的对象。所以,改变 person2 中的 student2 并不会影响到 person1:
但是,通过 clone() 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 clone() 方法,当嵌入的对象比较多的时候,就很容易出错了。
利用序列化的方式进行深拷贝,序列化时将对象写到流中便于传输,而反序列化时将对象从流中读取出来。
写入流中的对象就是对原始对象的拷贝。但是需要注意,每个要序列化的类都要实现 Serializable 接口,这个接口与 Cloneable 接口类似,都是标记型接口。
继续以 Person 类和 Student 举例:
Student 类:
/**
* student类
*
* @author qiaohaojie
* @date 2023/3/9 22:38
*/
@Data
public class Studentl implements Serializable {
private double score;
private String name;
public Studentl(double score, String name) {
this.score = score;
this.name = name;
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"score=" + score +
", name='" + name + '\'' +
'}';
}
}
Person 类:
/**
* person类
*
* @author qiaohaojie
* @date 2023/3/9 22:38
*/
@Data
public class Personl implements Serializable {
private int age;
private String name;
private Studentl student;
public Personl(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return super.toString().substring(18) + "{" +
"age=" + age +
", name='" + name + '\'' +
", student=" + student +
'}';
}
//深度拷贝
public Object deepClone() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
Person 类也需要实现 Serializable 接口,并且在该类中,增加了一个 deepClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。
测试类:
/**
* 序列化
*
* @author qiaohaojie
* @date 2023/3/9 22:41
*/
public class CloneTest4 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Personl person1 = new Personl(23, "青花椒");
Studentl student1 = new Studentl(93.2, "张三");
person1.setStudent(student1);
Personl person2 = (Personl) person1.deepClone();
System.out.println("深拷贝后:");
System.out.println("person1:" + person1); // person1:Personl@ee8f2be0{age=23, name='青花椒', student=Studentl@67e260bd{score=93.2, name='张三'}}
System.out.println("person2:" + person2); // person2:Personl@1f00b209{age=23, name='青花椒', student=Studentl@9853e6e6{score=93.2, name='张三'}}
Studentl student2 = person2.getStudent();
student2.setName("李四");
student2.setScore(88.8);
System.out.println("person2.变更后:");
System.out.println("person1:" + person1); // person1:Personl@ee8f2be0{age=23, name='青花椒', student=Studentl@67e260bd{score=93.2, name='张三'}}
System.out.println("person2:" + person2); // person2:Personl@1f00b209{age=23, name='青花椒', student=Studentl@9853e6e6{score=88.8, name='李四'}}
}
}
这种方式也可以实现深拷贝,但是由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 clone() 方法差很多。