Java 对象赋值与 clone 方法:浅拷贝与深拷贝解析
本文讲解了 Java 中自定义类型赋值的机制及 clone 方法的使用。默认赋值仅复制引用,导致对象共享内存。通过实现 Cloneable 接口并重写 clone 方法可实现对象复制。Object 类的 clone 默认为浅拷贝,即基本类型复制,引用类型仍共享地址。若需完全独立副本,需手动递归克隆引用对象以实现深拷贝。深拷贝确保原对象与拷贝对象在引用字段上互不影响,适用于复杂对象的独立复制场景。

本文讲解了 Java 中自定义类型赋值的机制及 clone 方法的使用。默认赋值仅复制引用,导致对象共享内存。通过实现 Cloneable 接口并重写 clone 方法可实现对象复制。Object 类的 clone 默认为浅拷贝,即基本类型复制,引用类型仍共享地址。若需完全独立副本,需手动递归克隆引用对象以实现深拷贝。深拷贝确保原对象与拷贝对象在引用字段上互不影响,适用于复杂对象的独立复制场景。

什么是赋值呢?
// 创建一个变量 a,值为 10
int a = 10;
// 把 a 的值赋值给 b
int b = a;
// 他们两个的值一样
System.out.println("a: " + a);
System.out.println("b: " + b);

int 是 Java 中的内置类型,我们自己创建的自定义类型(类)可以赋值吗?
同学们看一下,判断这是我们自定义类型的赋值吗?
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
public class Main {
public static void main(String[] args) {
// 创建一个对象
Student s1 = new Student("李华", 18);
// 把 s1 的值赋值给 s2
Student s2 = s1;
System.out.println(s1.toString());
System.out.println(s2.toString());
}
}
答:不是的,在上面的代码中,s1 和 s2 指向的是一个空间,s2 并没有属于自己的空间。
如图:

我们想要的是:s2 有自己的空间,并且空间的内容和 s1 一样(如下图)

自定义类型的赋值是:申请一个自己单独能管理的空间。
想要完成'完美'的赋值,就需要使用 clone() 方法。
Cloneable 接口: 只有实现了 Cloneable 接口的类才可以正常调用 clone() 方法。否则会抛出 CloneNotSupportedException 异常。
Cloneable 接口文档如下

重写 clone() 方法: Object 类的 clone() 方法是 protected,所以在自定义类中重写时,必须将其访问修饰符改为 public,才能从外部访问。
Object 类中的 Clone 方法:

总结: 当一个类需要写 clone 方法时,必须实现接口 Cloneable,并且重写 Object 类中的 clone 方法。
标题 1 的问题很严重,因为两个引用类型都指向一个空间,那么,当其中一个对象修改属性时另一个对象的属性也会被修改。
public class Main {
public static void main(String[] args) {
// 创建一个对象
Student s1 = new Student("李华", 18);
// 把 s1 的值赋值给 s2
Student s2 = s1;
System.out.println(s1.toString());
System.out.println(s2.toString());
// 修改 s1 的 name,s2 的 name 也修改了
s1.name = "王小明";
System.out.println("只修改 s1 的 name 为 王小明");
System.out.println(s1.toString());
System.out.println(s2.toString());
}
}

我们要做的是,进行赋值时给 s2 也开一个空间。

需要在让 Student 类实现 Cloneable 接口,重写 Object 中的 clone 方法,并且调用 clone 方法。
class Student implements Cloneable {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
public class Main {
public static void main(String[] args) {
try {
// 创建一个对象
Student s1 = new Student("李华", 18);
// 把 s1 的值赋值给 s2
Student s2 = s1.clone();
// 调用 object 中的 clone 方法
System.out.println(s1.toString());
System.out.println(s2.toString());
s1.name = ;
System.out.println();
System.out.println(s1.toString());
System.out.println(s2.toString());
} (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
在修改 s1 的 name 时,s2 就不会随着 s1 而变动了。

Object 类中的 clone() 方法用于创建当前对象的一个副本(即对象的浅拷贝)。它是一个受保护的方法(用 protected 修饰),所以默认情况下只有 Object 类本身及其子类可以调用。在实际开发中,clone() 方法常常被重写,以便提供更灵活的复制功能。
clone 方法在 object 类的声明如下:
protected Object clone() throws CloneNotSupportedException;
说明:
clone() 方法是 Object 类中定义的,用于返回一个与当前对象相同的副本。在默认实现中,它是浅拷贝,意味着它会复制对象的基本数据类型字段,但如果对象包含引用类型的字段,那么这些字段依然指向原来的对象。
浅拷贝指的是在复制对象时,只复制对象的基本数据类型字段和引用字段的引用(即内存地址),并没有复制引用字段所指向的对象本身。也就是说,原对象和拷贝对象会共享对引用类型字段(例如数组、集合等)的引用。这就导致了如果你修改原对象中的引用类型字段,拷贝对象中的相同字段也会被修改。
在上述的例子中,super.clone() 方法通过 Object 类的 clone() 方法创建一个新的 Student 对象。
String name = "***":这种方式使用字符串字面量创建字符串。
String name = new String(""):这种方式会在堆内存中创建一个新的 String 对象,其内容为 "",当 name 的内容改变时,会再创建一个空间,然后把新的引用(空间地址)赋值给 name。
由于 name(name="***")和 age 都是基本数据类型和不可变对象(如 String),拷贝的过程中,name 和 age 会被直接复制。
对于 name 字段,String 是不可变的,所以即使修改原对象的 name 字段,拷贝对象的 name 字段也不会受到影响。
但是,当我们在 Student 类中添加一个自定义的引用类型,再次仿照上述 Main() 类中的 main 方法操作,发生什么呢?
class Person {
public int number; // 电话号码
public Person(int number) {
this.number = number;
}
}
class Student implements Cloneable {
public String name;
public int age;
Person person;
public Student(String name, int age, int number) {
person = new Person(number);
this.name = name;
this.age = age;
}
@Override
public Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + ", age=" + age + ", number=" + person.number + '}';
}
}
public class Main {
public static void main(String[] args) {
{
(, , );
s1.clone();
System.out.println(s1.toString());
System.out.println(s2.toString());
s1.person.number = ;
System.out.println();
System.out.println(s1.toString());
System.out.println(s2.toString());
} (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
使用 clone 后,s1 和 s2 不是指向各自自己的空间了吗,为什么改变 s1 中的值而 s2 中的值也发生改变了呢?
这就是浅拷贝,浅拷贝指的是在复制对象时,只复制对象的基本数据类型字段和引用字段的引用(即内存地址),并没有复制引用字段所指向的对象本身。
在 Student 类中,用一个引用变量 person,创建 s1 时 person 指向自己的空间,person 存储的是引用(空间地址,比如是 0X555),当把 s1 克隆给 s2 时 s2 中的 person 存储也是 0X555。

为了解决这一问题,需要自己重写 clone 方法,并且手动给 person 开空间。
简单的来说,深拷贝就是解决浅拷贝存在的问题,为对象中的引用类型也开辟自己的空间,把类的赋值完美化。

为了解决浅拷贝遗留的问题,需要在 Person 类中也写一个 clone 方法:
// 实现接口
class Person implements Cloneable {
public int number; // 电话号码
public Person(int number) {
this.number = number;
}
// 重写 clone 方法
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone(); // 调用 clone 方法
}
}
需要在 Student 中改进 clone 方法的定义,使用 Person 中的 clone 方法:
public Student clone() throws CloneNotSupportedException {
// 创建一个 Student 临时对象 tmp
// 此时的 s.person 和 this.person 存储的相同的引用
Student tmp = (Student) super.clone();
// 使用 person 的 clone 方法创建一个新的对象
// s.person 指向的是新的对象
tmp.person = person.clone();
return tmp;
}
改为深拷贝的整体代码:
class Person implements Cloneable {
public int number; // 电话号码
public Person(int number) {
this.number = number;
}
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
class Student implements Cloneable {
public String name;
public int age;
Person person;
public Student(String name, int age, int number) {
person = new Person(number);
this.name = name;
this.age = age;
}
@Override
public Student clone() throws CloneNotSupportedException {
// 创建一个 Student 临时对象 tmp
// 此时的 s.person 和 this.person 存储的相同的引用
Student tmp = (Student) super.clone();
// 使用 person 的 clone 方法创建一个新的对象
// s.person 指向的是新的对象
tmp.person = person.clone();
return tmp;
}
@Override
public String {
+ + name + + + age + + person.number + ;
}
}
{
{
{
(, , );
s1.clone();
System.out.println(s1.toString());
System.out.println(s2.toString());
s1.person.number = ;
System.out.println();
System.out.println(s1.toString());
System.out.println(s2.toString());
} (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行程序:

显然,当 s1 改变时 s2 不会改变,s1 中的引用类型 person 和 s2 中的 person 指向不同的空间。

关键点: 深拷贝的核心是递归地复制引用类型字段所指向的对象,而不仅仅是复制它们的引用。 浅拷贝会让原对象和拷贝对象共享引用类型字段,而深拷贝会确保原对象和拷贝对象的引用类型字段相互独立。
在上面的例子中,Student 类中的 person 字段是通过 clone() 方法进行深拷贝的,修改原对象的 person 字段不会影响拷贝对象。
深拷贝的应用:
总结: 深拷贝是对象复制中的一种高级技术,能够确保对象之间完全独立,特别适用于包含引用类型字段的复杂对象。当需要确保每个对象的字段都能被独立地复制,并且修改一个对象不会影响另一个对象时,深拷贝是必不可少。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online