0%

Java专题:对象拷贝

预备知识

创建对象的方式

  1. new
  2. Class.newInstance()
  3. Constructor.newInstance()
  4. Object.clone()
  5. 反序列化
  6. 对于 String 及基本类型封装类:字面量、表达式

数据类型

Java 的数据类型可分为两大类:基本数据类型和引用类型。

简单来说,基本数据类型直接存储于栈中;而引用类型仅将引用存储于栈中,真实对象存储在堆中。

关于拷贝

所谓“拷贝”,或称“克隆”,从字面不难理解,就是根据一个蓝本对象,创造一个相同的克隆对象。

拷贝操作后的结果一定是生成一个新的克隆对象,但是拷贝操作的具体实现路径不是唯一的。

对于基本类型比较简单,可以认为基本类型的实例变量就是数据本身,因此,拷贝基本类型实例变量只能是生成一个相同的副本。这不会产生分歧,分歧体现在引用类型的实例变量的拷贝上。

但是,对于引用类型实例变量而言,它仅仅是一个引用,或者说“地址”,对于它拷贝存在两种处理方式:1. 简单拷贝引用类型实例变量,其结果是得到一个引用,该引用与原引用指向堆中相同的真实对象;2. 根据引用类型实例变量引用的对象生成一个相同的对象(可能存在递归的克隆操作)。这就是产生分歧的源头。

浅拷贝 vs. 深拷贝

如上所述,对象拷贝就有了两种实现方式。

对于引用类型实例变量仅拷贝其引用地址的实现方式,称为“浅拷贝”——它是 Java 拷贝方法的默认行为——而生成新的引用的对象的实现方式则称为“深拷贝”。

从对象结构上来看,深拷贝生成的克隆对象与蓝本对象完全独立,但是浅拷贝生成的克隆对象与蓝本对象共享引用类型实例变量引用的真实对象。

通常需要深拷贝

我们需要哪种拷贝?

要回答这个问题,需要我们先回答另一个问题:我们为什么拷贝?

回想一下,我们拷贝得到一个克隆对象后,用它做了什么?

也许将克隆对象做为一个快照,稍后与蓝本对象对比差异;也许为了避免对蓝本对象的修改,而修改克隆对象……

总之,大多数情况下,事实上我们希望得到的克隆对象应该是独立于蓝本对象的,换句话说,我们需要“深拷贝”。

默认却是浅拷贝

我们几乎总是需要深拷贝,但 Java 默认提供浅拷贝,这是为什么?

从设计上来看,浅拷贝比深拷贝简单易实现,且支持从简单的浅拷贝扩展到深拷贝是合理的。

另一方面,由于对象间的引用可能是极其复杂的,所以深拷贝实现可能会很复杂,甚至实现一种通用的深拷贝是不可能的。比如,拷贝一个循环链表的结点,可能引发无限递归拷贝的问题。要解决这些问题,可能会涉及循环引用检测,或者对象图遍历等等方面,复杂度、性能等方面都可能面临各种考验。

而且,话说回来,完全的深拷贝也许也不是我们想要的。

所以,提供一个浅拷贝实现,并支持深拷贝扩展应该算是比较恰当的设计。

clone() 方法

Java 使用 clone() 方法拷贝对象,该方法是 Object 对象声明的,如下:

1
protected native Object clone() throws CloneNotSupportedException;

注意:clone() 方法是一个本地方法,换句话说,它不是 Java 本身实现的。

实现深拷贝

Java 扩展实现深拷贝是容易的,只需要被拷贝对象对应的类实现 Cloneable 这个标记接口,并重写 clone() 方法,将访问权限放宽为 public 即可。

定制的 clone() 方法大概内容如下(这里不给示例代码了):

  1. 调用 super.clone() 拷贝当前对象;
  2. 拷贝创建新的实例变量对象并设入当前克隆对象。

注意:实例变量涉及到的自定义类也应该实现深拷贝;如果是通用集合类型还需要自行创建集合对象,并深拷贝其中元素并添加入新集合中。

clone() 方法的可见性

读者可能注意到,clone() 方法是在 Object 中声明的 protected 方法,而所有类都是 Object 的子类,但如果我们在没有实现 Cloneable 的类对象上调用 clone() 方法会得到一个编译错误,提示 clone() 不可见。

如果读者对其有所疑惑,说明对 protected 访问权限的理解有所偏差。

根据 Java 语言规范:对象的 protected 成员或构造器在声明它的包的外部,只能被负责实现该对象的代码所访问。

假设我们声明一个类 A,对于 Object.clone() 而言,它就是存在于声明它的包(java.lang)外部的 protected 成员。而何为“负责实现该对象的代码”呢?就是类 A 代码。因此,我们可以在类 A 中调用 Object.clone(),但不能在类 B 中调用 A.clone()

因而,为了使声明的类的 clone() 方法可见,只能重写 clone() 并放宽访问权限为 public

但是,如果我们仅仅重写 clone() 方法但不实现 Cloneable 接口,代码是可以通过编译,但会得到一个运行时异常,这是因为 Object.clone() 方法会检查被拷贝对象是否实现 Cloneable 接口。