关于 Java 克隆
本文探讨 Java 中的对象克隆问题。首先说明基本类型赋值实现的是值拷贝,而自定义对象赋值只是引用拷贝,无法真正独立。接着深入区分浅拷贝与深拷贝的核心区别:浅拷贝仅复制对象本身及其基本类型字段,深拷贝则递归复制所有成员变量直至基本类型。通过重写 clone() 方法并实现 Cloneable 接口可以完成对象拷贝,同时需处理 CloneNotSupportedException 异常。
补充关于 Java 克隆的一点点。
一、开始
先来看一段代码:
int a = 1;
int b = a;
b = 2;
System.out.println(a); // 输出: 1
System.out.println(b); // 输出: 2a 的值并没有因为复制给 b 然后 b 被修改而发生改变。
这就实现了 b 对 a 的一份独立拷贝。对于 byte, short, int, char 等基本数据类型,直接赋值都适用这种行为(你可以将其理解为基本类型的深拷贝)。
但是,如果要复制一份自定义的“对象”,简单的赋值操作就无法达到这种独立性了,这涉及到浅拷贝与深拷贝的区别。
何为深浅呢? 一个重要的标识:是否完全复制了一个新的对象,并在堆中为其申请了新的内存空间。
在 Java 中,对象的直接赋值(Student s2 = s1;)只是将两个栈中的引用指向了堆内存中的同一块存储空间。所以一个引用的操作必定会影响原数据。
理论解释:
- 对于基本类型,变量直接存储在栈中,赋值操作是直接将值复制一份,互不影响;
- 对于自定义对象,由于引用存储在栈中,而对象实际数据分配在堆中,默认的赋值或浅拷贝只拷贝了引用,并没有在堆中去申请新的对象空间。
- 特殊的如
String,虽然是对象,但由于其不可变性(Immutable)及常量池机制,直接赋值时其行为表现得像基本类型一样安全。
二、浅拷贝 (Shallow Copy)
通过实现 Cloneable 接口并重写 clone() 方法,可以实现对象拷贝。默认的 super.clone() 提供的就是浅拷贝。
package com.xie.core.base;
class School {
private String name;
// 省略 getter/setter
}
class Student implements Cloneable {
private School school;
private String name;
// 省略 getter/setter
/**
* 浅拷贝实现
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloneTest01 {
public static void main(String[] args) throws Exception {
School school = new School();
school.setName("Thank School");
Student student1 = new Student();
student1.setName("Thank");
student1.setSchool(school);
// 浅拷贝
Student student2 = (Student) student1.clone();
student2.setName("Chunhua");
// 修改 student2 的 school 属性
student2.getSchool().setName("Chunhua School");
System.out.println(student1.getName() + ", " + student1.getSchool().getName());
System.out.println(student2.getName() + ", " + student2.getSchool().getName());
}
}输出结果为:
Thank, Chunhua School
Chunhua, Chunhua School我分别打印了 school 的内存地址(toString()):
student1 --> school: com.xie.core.base.School@762efe5dstudent2 --> school: com.xie.core.base.School@762efe5d
可以看到,student2 的拷贝只是对 String 类型的 name 实现了独立拷贝,而对 School 对象类型的属性,两者依然指向同一个内存地址。这就叫浅拷贝。
三、深拷贝 (Deep Copy)
为了解决上面的问题,我们需要在拷贝 Student 的同时,连同它内部的 School 对象也一并拷贝。
package com.xie.core.base;
class School implements Cloneable {
private String name;
// 省略 getter/setter
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
class Student implements Cloneable {
private School school;
private String name;
// 省略 getter/setter
/**
* 深拷贝实现
*/
@Override
protected Object clone() throws CloneNotSupportedException {
Student o = (Student) super.clone();
// 手动调用内部对象 School 的 clone 方法实现深拷贝
o.school = (School) this.school.clone();
return o;
}
}
public class CloneTest01 {
public static void main(String[] args) throws Exception {
School school = new School();
school.setName("Thank School");
Student student1 = new Student();
student1.setName("Thank");
student1.setSchool(school);
Student student2 = (Student) student1.clone();
student2.setName("Chunhua");
student2.getSchool().setName("Chunhua School");
System.out.println(student1.getName() + ", " + student1.getSchool().getName());
System.out.println(student2.getName() + ", " + student2.getSchool().getName());
}
}输出结果为:
Thank, Thank School
Chunhua, Chunhua School再次打印 school 的内存地址:
student1 --> school: com.xie.core.base.School@762efe5dstudent2 --> school: com.xie.core.base.School@41a4555e
此时它们是两个不同的对象。符合“完全复制一个新的对象,并申请新的内存空间”的定义,这就是深拷贝。
四、利用序列化实现深拷贝
如果一个对象内部嵌套了多层其他对象,一层层去重写 clone() 方法将是一场噩梦。此时可以利用 Java 的序列化(Serialization)来实现深拷贝。
把对象写到流里的过程是串行化(Serialization),把对象从流中读出来的过程叫反序列化(Deserialization)。 应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于 JVM 里面。因此,先使对象实现
Serializable接口,然后把对象写到一个流里,再从流里读出来便可以完美重建出一个全新的对象。
这种方式不需要实现 Cloneable 接口,但是所有涉及的类必须实现 Serializable 接口。
package com.xie.core.base;
import java.io.*;
class School implements Serializable { // 实现序列化接口
private String name;
// 省略 getter/setter
}
class Student implements Serializable { // 实现序列化接口
private School school;
private String name;
// 省略 getter/setter
/**
* 串行化深复制
*/
public Object deepClone() throws IOException, ClassNotFoundException {
// 写入字节流
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bo);
oos.writeObject(this);
// 从字节流读出
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream osi = new ObjectInputStream(bi);
return osi.readObject();
}
}
public class CloneTest01 {
public static void main(String[] args) throws CloneNotSupportedException, ClassNotFoundException, IOException {
School school = new School();
school.setName("Thank School");
Student student1 = new Student();
student1.setName("Thank");
student1.setSchool(school);
Student student2 = (Student) student1.deepClone();
student2.setName("Chunhua");
student2.getSchool().setName("Chunhua School");
System.out.println(student1.getName() + ", " + student1.getSchool().getName());
System.out.println(student2.getName() + ", " + student2.getSchool().getName());
}
}总结
在内存中通过字节流的拷贝是比较容易实现且稳妥的。把母对象写入到一个字节流中,再从字节流中将其读出来,就可以创建一个新的对象,该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。
【现代开发避坑与进阶提示】: 虽然利用 Java 原生序列化可以实现深拷贝,但其性能较低且代码繁琐。在现代企业级开发中,我们很少再手写原生的
clone()或序列化流。常用的替代方案有:
- JSON 序列化方式:利用
Fastjson、Jackson或Gson,将对象转为 JSON 字符串,再反序列化为对象(代码极简,推荐)。- 工具类:如果是单纯的属性拷贝(VO/DTO 转换),直接使用
Spring BeanUtils或MapStruct。- Apache Commons Lang:直接调用
SerializationUtils.clone(object)即可一行代码完成基于序列化的深拷贝。