Java 克隆示例

2025年3月17日 | 阅读 12 分钟

对象克隆(Object cloning)是指创建对象的精确副本。它会创建一个与当前对象同类的类的新实例,并用当前对象中的对应字段中的所有对象来初始化它。

使用赋值运算符创建引用变量的副本,

在 Java 中,没有运算符可以创建对象的副本。与 C++ 不同,在 Java 中,如果我们使用赋值运算符,它将创建引用变量的副本而不是对象的副本。可以通过一个示例来解释这一点。以下示例显示了相同的内容。

示例

输出

Java Clone Examples

使用 clone() 方法创建副本

要复制其对象的类必须在其本身或其某个父类中定义一个公共的 clone 方法。

每个实现 clone() 的类都应调用 super.clone() 以获取克隆的对象引用。

要复制的对象所在的类还必须实现 Java.lang.Cloneable 接口。否则,当访问该类对象的克隆方法时,将抛出 CloneNotSupportedException。

语法

protected Object clone() throws CloneNotSupportedException

对象克隆的类型

  1. 浅克隆(Shallow Cloning):每当执行默认克隆方法时,就会发生浅克隆。浅克隆基本上会将对象的所有字段复制到新实例中。clone() 支持浅克隆。
  2. 深克隆(Deep Cloning):现在,每次我们不执行默认的克隆方法时,我们都在进行深克隆。深克隆按照我们的需求工作。现在,这里的关键区别在于,深克隆复制所有字段以及对象的值,而浅克隆仅复制字段。
  3. 懒克隆(Lazy Cloning):Java 中支持第三种克隆类型,它是上述两种克隆类型的组合;它称为懒克隆。并且,没有特定的规则规定何时使用哪种克隆类型;这取决于我们的需求。

当我们使用 clone 方法的默认实现时,我们会得到对象的浅拷贝,这意味着它会创建一个新实例,并将对象的所有字段复制到该新实例中,然后将其作为对象类型返回。我们需要将其显式地转换回我们的原始对象。这是对象的浅副本。

Object 类的 clone() 方法支持对象的浅副本。如果对象在浅副本中包含原始类型和非原始类型或引用类型变量,则克隆的对象也引用与原始对象引用的相同对象,因为只有对象引用被复制,而不是被引用的对象本身。

这就是为什么在 Java 中称为浅拷贝或浅克隆。如果只有原始类型字段或不可变对象存在,那么在 Java 的浅副本和深副本之间就没有区别。

clone() 方法示例 - 浅拷贝

在下面的代码示例中,clone() 方法创建了一个具有不同 hashCode 值的新对象,这意味着它在不同的内存区域。但是,由于 Test 对象 c 位于 Test2 中,因此原始类型已实现深拷贝,但此 Test 对象 c 仍在 t1 和 t2 之间共享。为了解决这个问题,我们显式地为对象变量 c 创建了深拷贝。

创建浅拷贝的示例程序

输出

Java Clone Examples

在上面的示例中,t1.clone 返回对象 t1 的浅副本。要获得对象的深拷贝,在获得副本后,需要在 clone 方法中进行一些更改。

clone() 方法示例 - 深拷贝

如果我们想创建一个对象 X 的深拷贝并将其放入另一个对象 Y 中,那么将创建对任何被引用对象字段的新副本,并将这些引用放入对象 Y 中。这意味着在对象 X 或 Y 的被引用对象字段中所做的任何更改都将仅反映在该对象中,而不会反映在另一个对象中。在下面的示例中,我们创建了对象的深拷贝。

深拷贝会复制所有字段并创建被引用的动态分配内存的副本。当复制一个对象以及它引用的对象时,就会发生深拷贝。

创建深拷贝的示例程序

输出

Java Clone Examples

在上面的示例中,我们可以看到已经为 HelloWorld 类分配了一个新对象来复制将返回给 clone 方法的对象。因此,t2 将获得对象 t1 的深拷贝。所以 t2 对 'c' 对象字段所做的任何更改都不会反映在 t1 中。

浅拷贝和深拷贝的区别

  1. 浅拷贝是复制对象的进程,并且是克隆中默认遵循的方式。在此方法中,旧对象 X 的字段被复制到新对象 Y。在复制对象类型字段时,引用被复制到 Y,即对象 Y 将指向与 X 指向的相同内存地址。如果字段值是原始类型,它会复制原始类型的值。
  2. 因此,在对象 X 或 Y 的被引用对象中所做的任何更改都将反映在其他对象中。
  3. 浅拷贝的成本较低且易于创建

深拷贝和浅拷贝相同的场景

1. 在 Strings 中

我们需要了解复制字符串时会发生什么。我们都知道字符串被视为 Java.lang 包中 String 类的对象。因此,与其他对象一样,当我们进行复制时,会复制引用。

示例

输出

Java Clone Examples

解释

程序的输出显示引用变量 s1 和 s2 显示的哈希码相同。这意味着引用变量 obj1 和 obj2 指向相同的内存地址。但是,问题是,我们可以说我们在上面的程序中进行了浅拷贝吗,因为引用是相同的?答案是否定的。以下示例为我们提供了足够的证据来验证给出的答案。

示例

输出

Java Clone Examples

解释

程序的输出告诉我们,引用变量 s1 显示的哈希码与引用变量 s2 显示的哈希码不相等。此外,使用引用变量 s2 所做的更改不会由引用变量 s1 显示。这是因为 Java 中的字符串始终是不可变的。因此,当 s2 更改 s1 的内容时,它最终会创建一个全新的字符串。因此,先前的字符串保持不变,引用变量 obj2 指向新的字符串对象所在的新内存位置。

我们已经看到,字符串的更改会导致创建新的字符串对象。因此,复制字符串不能称为深拷贝,也不能称为浅拷贝。事实上,当我们在 Java 中处理字符串时,深拷贝和浅拷贝之间没有区别。

2. 在原始数据类型中

我们需要了解复制原始数据类型时会发生什么。与字符串不同,原始数据类型不是对象。但是,与字符串一样,在原始数据类型中没有深拷贝或浅拷贝的概念。请注意下面的示例。

输出

Java Clone Examples

解释

当 s2 的值被更新时,它不会影响 s1 的值。这是因为 y 已经有了自己的内存分配。它不指向 s1 的内存地址。因此,赋值 s2 = s1;只是将 s1 的值复制到 s2。因此,y 中的任何更新都不会影响 x。类似的约定也适用于其他原始数据类型。在这里,s2 的内存分配也是自动发生的。

在 Java 中,没有标准的规定何时使用浅拷贝以及何时使用深拷贝。这取决于程序员或设计师决定他们想要使用什么。因此,建议先理解需求,然后再明智地选择深拷贝和浅拷贝。

clone() 方法的优点

  1. 如果我们使用赋值运算符来分配引用另一个引用变量的对象,那么它将指向旧对象的相同内存位置,而不会创建对象的新副本。因此,引用变量中的任何更改都会反映在原始对象中。
  2. 如果我们使用复制构造函数,我们需要显式复制所有数据。例如,我们需要在构造函数中显式地重新分配类的所有字段。但是,在 clone 方法中,这项创建新副本的工作由方法本身完成。因此,为了避免额外的处理,我们使用对象克隆。
  3. 您无需编写冗长而乏味的 C++ 代码。只需使用一个具有 4 或 5 行长的 clone() 方法的抽象类。
  4. 它是复制对象的​​最简单、最有效的方法,尤其是在将其应用于一个完善或旧项目时。只需定义一个父类,在其上实现 Cloneable,给出 clone() 方法的定义,任务就完成了。
  5. clone() 是复制数组最快的方法。

clone() 方法的缺点

  1. 要使用 Object.clone() 方法,我们需要修改代码的许多语法,例如实现 Cloneable 接口,定义 clone() 方法并处理 CloneNotSupportedException,最后,调用 Object.clone() 等等。
  2. 我们需要实现一个可克隆接口,而它本身没有任何方法。我们只需要使用它来让 JVM 知道我们可以对我们的对象执行 clone()。
  3. clone() 是受保护的,因此我们需要提供自己的 clone() 并从中间接调用 Object.clone()。
  4. clone() 不会调用任何构造函数,因此我们对对象创建没有任何控制。
  5. 要在子类中编写 clone 方法,其超类必须在其类中定义 clone() 方法或从另一个父类继承它。否则,super.clone() 链将失败。
  6. clone() 只支持浅拷贝,但如果我们需要深拷贝,则必须覆盖它。