Java的对象克隆

简介: Object 类提供的 clone() 方法是 “浅拷贝”,并没有克隆对象中引用的其他对象,原对象和克隆的对象仍然会共享一些信息。深拷贝指的是:在对象中存在其他对象的引用的情况下,会同时克隆对象中引用的其他对象,原对象和克隆的对象互不影响。

本节我们会讨论 Cloneable 接口,这个接口指示一个类提供了一个安全的 clone() 方法。

Object 类提供的 clone() 方法是 “浅拷贝”,并没有克隆对象中引用的其他对象,原对象和克隆的对象仍然会共享一些信息。深拷贝指的是:在对象中存在其他对象的引用的情况下,会同时克隆对象中引用的其他对象,原对象和克隆的对象互不影响。

介绍克隆

要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用(见图 6-1)。这说明,任何一个变量改变都会影响另一个变量。

Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(lO); // oops-also changed original

如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用 clone() 方法。

Employee copy = original.clone();
copy.raiseSalary(lO); // OK original unchanged

image-20230418124610231.png


不过并没有这么简单。clone() 方法是 Object 的一个 protected 方法,这说明你的代码不能直接调用这个方法。只有 Employee 类可以克隆 Employee 对象(Object 类不可以克隆 Employee 类)。这个限制是有原因的。想想看 Object 类如何实现 clone()。Object 类它对于这个对象一无所知,所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。

class Employee {
    // instance fields
    private String name;
    private double salary;
    private Date hireDay;

    // constructor
    public Employee(String n, double s, int year, int month, int day) {
        this.name = n;
        this.salary = s;
        this.hireDay = new Date(year, month, day);
    }

    // a method
    public String getName() {
        return name;
    }
    // more methods
}

图 6-2 显示了使用 Object 类的 clone() 方法克隆一个 Employee 对象会发生什么。可以看到,默认的克隆操作是 “浅拷贝”,并没有克隆对象中引用的其他对象。

浅拷贝会有什么影响吗?这要看具体情况。

  • 如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如 String,就是这种情况。或者在对象的生命周期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的(子对象虽然是可变的,但是在在对象的生命周期中,子对象的数据域没有发生改变)。
  • 不过,通常子对象都是可变的,必须重新定义 clone() 方法来建立一个深拷贝,同时克隆所有子对象。在这个例子中,hireDay 域是一个 Date,这是可变的,所以它也需要克隆。(出于这个原因,这个例子使用 Date 类型的域而不是 LocalDate 来展示克隆过程。如果 hireDay 是不可变的 LocalDate 类的一个实例,就无需我们做任何处理了。)

image-20230418124817828.png


对于每一个类,需要确定:

  1. 默认的 clone() 方法是否满足要求;
  2. 是否可以在可变的子对象上调用 clone() 方法来修补默认的 clone() 方法;
  3. 是否不该使用 clone() 方法

实际上第 3 个选项是默认选项。如果选择第 1 项或第 2 项,类必须:

  • 实现 Cloneable 接口;
  • 重新定义 clone() 方法,并指定 public 访问修饰符。

Cloneable 接口

Cloneable 接口的出现与接口的正常使用并没有关系。具体来说,Cloneable 接口没有指定 clone() 方法,clone() 方法是从 Object 类继承的。Cloneable 接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很 “偏执”,如果一个对象请求克隆,但没有实现 Cloneable 接口,就会生成一个受检异常(CloneNotSupportedException 异常)。

如果在一个对象上调用 clone() 方法,但这个对象的类并没有实现 Cloneable 接口,Object 类的 clone() 方法就会拋出一个 CloneNotSupportedException。

注释:Cloneable 接口是 Java 提供的一组标记接口(tagging interface)之一。应该记得:

  • Comparable 等接口的通常用途是确保一个类实现一个或一组特定的方法。
  • 标记接口不包含任何方法,标记接口唯一的作用就是允许在类型查询中使用 instanceof:if (obj instanceof Cloneable) {}

建议你自己的程序中不要使用标记接口。


即使 clone() 的默认(浅拷贝)实现能够满足要求,还是需要实现 Cloneable 接口,将 clone() 方法重新定义为 public, 再调用 super.clone()。下面给出一个例子:

class Employee implements Cloneable {
    // raise visibility level to public, change return type
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone();
    }
}

与 Object.clone() 提供的浅拷贝相比,前面看到的 clone() 方法并没有为它增加任何功能。这里只是让这个方法是公有的。要建立深拷贝,还需要做更多工作,克隆对象中可变的实例域。下面来看创建深拷贝的 done() 方法的一个例子:

class Employee implements Cloneable {
    // ...
    public Employee clone() throws CloneNotSupportedException {
        // call Object.clone()
        Employee cloned = (Employee) super.clone();
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }
}

如果在一个对象上调用 clone() 方法,但这个对象的类并没有实现 Cloneable 接口,Object 类的 clone() 方法就会拋出一个 CloneNotSupportedException。当然,Employee 和 Date 类实现了 Cloneable 接口,所以不会抛出这个异常。不过, 编译器并不了解这一点,因此,我们声明了这个异常。


捕获这个异常是不是更好一些?

public Employee clone() {
    try {
        Employee cloned = (Employee) super.clone();
        // ...
        return cloned;
    } catch (CloneNotSupportedException e) {
        return null;
    }
    // this won't happen, since we are Cloneable
}

捕获异常这非常适用于 final 类。否则,最好还是保留 throws 说明符。这样就允许子类在不支持克隆时选择抛出一个 CloneNotSupportedException()。

子类的克隆

必须当心子类的克隆。例如,一旦为 Employee 类定义了 clone() 方法,任何人都可以用它来克隆 Manager 对象(Manager 类是 Employee 类的子类)。Employee 克隆方法能完成工作吗?这取决于 Manager 类的域。在这里是没有问题的,因为 bonus 域是基本类型。但是 Manager 可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正 clone() 方法让它正常工作。出于这个原因, 在 Object 类中 clone() 方法声明为 protected。不过,如果你希望类用户调用 clone(),就不能这样做。

参考资料

《Java核心技术卷一:基础知识》(第10版)第 6 章:接口、lambda 表达式与内部类 6.2.3 对象克隆

相关文章
|
5月前
|
Java
深入JavaSE:详解Java对象的比较。
总的来说,Java对象的比较就像海洋生物的比较,有外在的,有内在的,有面对所有情况的,也有针对特殊情况的。理解并掌握这些比较方式,就能更好地驾驭Java的世界,游刃有余地操作Java对象。
86 12
|
6月前
|
编解码 JavaScript 前端开发
【Java进阶】详解JavaScript的BOM(浏览器对象模型)
总的来说,BOM提供了一种方式来与浏览器进行交互。通过BOM,你可以操作窗口、获取URL、操作历史、访问HTML文档、获取浏览器信息和屏幕信息等。虽然BOM并没有正式的标准,但大多数现代浏览器都实现了相似的功能,因此,你可以放心地在你的JavaScript代码中使用BOM。
164 23
|
6月前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
125 1
|
7月前
|
设计模式 缓存 Java
重学Java基础篇—Java对象创建的7种核心方式详解
本文全面解析了Java中对象的创建方式,涵盖基础到高级技术。包括`new关键字`直接实例化、反射机制动态创建、克隆与反序列化复用对象,以及工厂方法和建造者模式等设计模式的应用。同时探讨了Spring IOC容器等框架级创建方式,并对比各类方法的适用场景与优缺点。此外,还深入分析了动态代理、Unsafe类等扩展知识及注意事项。最后总结最佳实践,建议根据业务需求选择合适方式,在灵活性与性能间取得平衡。
373 3
|
6月前
|
存储 缓存 Java
理解Java引用数据类型:它们都是对象引用
本文深入探讨了Java中引用数据类型的本质及其相关特性。引用变量存储的是对象的内存地址而非对象本身,类似房子的地址而非房子本身。文章通过实例解析了引用赋值、比较(`==`与`equals()`的区别)以及包装类缓存机制等核心概念。此外,还介绍了Java引用类型的家族,包括类、接口、数组和枚举。理解这些内容有助于开发者避免常见错误,提升对Java内存模型的掌握,为高效编程奠定基础。
275 0
|
6月前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
125 0
|
7月前
|
存储 算法 安全
Java对象创建和访问
Java对象创建过程包括类加载检查、内存分配(指针碰撞或空闲列表)、内存初始化、对象头设置及初始化方法执行。访问方式有句柄和直接指针两种,前者稳定但需额外定位,后者速度快。对象创建涉及并发安全、垃圾回收等机制。
Java对象创建和访问
|
9月前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
9月前
|
安全 Java
Object取值转java对象
通过本文的介绍,我们了解了几种将 `Object`类型转换为Java对象的方法,包括强制类型转换、使用 `instanceof`检查类型和泛型方法等。此外,还探讨了在集合、反射和序列化等常见场景中的应用。掌握这些方法和技巧,有助于编写更健壮和类型安全的Java代码。
464 17
|
8月前
|
存储 Java
Java中判断一个对象是否是空内容
在 Java 中,不同类型的对象其“空内容”的定义和判断方式各异。对于基本数据类型的包装类,空指对象引用为 null;字符串的空包括 null、长度为 0 或仅含空白字符,可通过 length() 和 trim() 判断;集合类通过 isEmpty() 方法检查是否无元素;数组的空则指引用为 null 或长度为 0。