java 核心技术-12版 卷Ⅰ- 6.1.9 对象克隆

createh52个月前 (02-05)技术教程12

原文

6.1.9 对象克隆

本节我们会讨论 Cloneable 接口,这个接口表示一个类提供了一个安全的 clone 方法。由于克隆并不太常见,而且有关的细节技术性很强,你可能只是想稍做了解,等真正需要时再深人学习。

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

6-1 拷贝和克隆

var original = new Employee("John Public”,50000);

Employee copy = original;

copy.raiseSalary(10); // oops--aso changed original // copy的修改会导致original也修改

如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后它们的状态可能不同,这种情况下就要使用 clone 方法(即克隆)。

Employee copy =original.clone();

copy.raiseSalary(10); // OK--original unchanged // original 未改变

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

为了更直观地说明这个问题,考虑第4章介绍过的 Eployee 类。图 6-2 显示了使用bject类的 clone 方法克隆这样一个Eployee 对象会发生什么。可以看到,默认的克隆操作是“浅拷贝”并没有克隆对象中引用的其他对象。(这个图显示了一个共享的 Date 对象。出于某种原因(稍后就会解释这个原因),这个例子使用了 Employee 类的老版本,其中的雇佣日期仍用 Date 表示。

6-2 浅拷贝

浅拷贝会有什么影响吗?这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如 String,就是这种情况;或者,在对象的生命期中,子对象一直保持不变,没有更改器方法改变它,也没有方

法会生成它的引用,这种情况下同样是安全的。

不过,通常子对象都是可变的,必须重新定义 Clone 方法来建立一个深拷贝 (deep copy),

这会克隆所有子对象。在这个例子中,hireDay字段是一个 Date,这是可变的,所以它也必须克隆。(正是由于这个原因,这个例子使用 Date 类型的字段而不是 LocalDate 来展示克隆过程。如果 hireDay 是不可变的 LocalDate 类的一个实例,就无须我们做任何操作了。)

对于每一个类,需要确定以下选项是否成立:

1.默认的 clone 方法就能满足要求;

2.可以在可变的子对象上调用 clone 来弥补默认的 clone 方法;

3.不该使用 clone。

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

1.实现 Cloneable接口;

2.重新定义 clone 方法,并指定 public 访问修饰符。

注释: Object 类中的 clone 方法声明为 protected,所以你的代码不能直接调用 anObject.clone()。但是,不是所有子类都能访问受保护方法吗? 不是所有类都是 Object 的子类吗?幸运的是,受保护访问的规则比较微妙 (见第 5章)。子类只能调用受保护的 clone方法来克隆它自己的对象。必须重新定义 clone 为 public 才允许所有方法克隆对象。

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

注释:Cloneable接口是Java 提供的少数标记接口 (tagging interface)之一。(有些程序员称之为记号接口(marker interface)。)应该记得,Comparable 等接口的通常用途是确保一个类实现一个特定的方法或一组方法。标记接口不包含任何方法,它唯一的作用就是允许在类型查询中使用 instanceof:

if (obi instanceof Cloneable) ...

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

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

Bash
class Employee implements Cloneable{
// public access, change return type
  public Employee clone() throws CloneNotSupportedException{
  		return (Employee) super.clone();
  }

}

注释:在Java 1.4之前,clone 方法的返回类型总是 Object,而现在可以为你的 clone方法指定正确的返回类型。这是协变返回类型的一个例子(参见第5章)。

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

Bash
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;
    
    }

}

Object类的 clone 方法有可能抛出一个
CloneNotSupportedException,如果在一个对象上调用clone,但这个对象的类并没有实现 Cloneable 接口,就会发生这种情况。当然,Employee 和 Date类实现了 Cloneable 接口,所以不会抛出这个异常。不过,编译器并不知道这一点,因此,我们声明了这个异常:

public Employee clone() throws (loneNotSupportedException

注释:捕获这个异常是不是更好一些?(关于捕获异常的详细介绍请参见第 7章。

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

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

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

要不要在自己的类中实现 clone 呢?如果你的客户需要建立深拷贝,可能就应当实现这个方法。有些人认为应该完全避免使用 clone,而实现另一个方法来达到同样的目的。clone相当别扭,这一点我们也同意,不过如果让另一个方法来完成这个工作,还是会遇到同样的问题。毕竟,克隆没有你想象巾那么常用。标准库中只有不到 5% 的类实现了 clone。

程序清单6-4 中的程序克隆了 Employee类(见程序清单6-5)的一个实例,然后调用两个更改器方法。raiseSalary 方法会改变 salary字段的值,而 setHireDay 方法会改变 hireDay字段的状态。这两个更改器方法都不会影响原来的对象,因为 clone 定义为建立一个深拷贝。

注释: 所有数组类型都有一个公共的 clone 方法,而不是受保护的。可以用这个方法建立一个新数组,包含原数组所有元素的副本。例如;

int[] luckyNumbers = ( 2,3,5,7,11,13 );

int[] cloned = luckyNumbers.clone();

cloned[5] = 12; // doesn't change luckyNumbers(5]

注释:卷2的第2章将展示克隆对象的另一种机制,其中使用了 Java 的对象串行化特性。这个机制很容易实现,而且很安全,但效率不高。

程序清单6-4 clone/CloneTest.java

Bash
package clone;

/**
 * This program demonstrates cloning.
 * @version 1.11 2018-03-16
 * @author Cay Horstmann
 */
public class CloneTest
{
   public static void main(String[] args) throws CloneNotSupportedException
   {
      var original = new Employee("John Q. Public", 50000);
      original.setHireDay(2000, 1, 1);
      Employee copy = original.clone();
      copy.raiseSalary(10);
      copy.setHireDay(2002, 12, 31);
      System.out.println("original=" + original);
      System.out.println("copy=" + copy);
   }
}

程序清单6-5 clone/Employee.java

Bash
package clone;

import java.util.Date;
import java.util.GregorianCalendar;

public class Employee implements Cloneable
{
   private String name;
   private double salary;
   private Date hireDay;

   public Employee(String name, double salary)
   {
      this.name = name;
      this.salary = salary;
      hireDay = new Date();
   }

   public Employee clone() throws CloneNotSupportedException
   {
      // call Object.clone()
      Employee cloned = (Employee) super.clone();

      // clone mutable fields
      cloned.hireDay = (Date) hireDay.clone();

      return cloned;
   }

   /**
    * Set the hire day to a given date. 
    * @param year the year of the hire day
    * @param month the month of the hire day
    * @param day the day of the hire day
    */
   public void setHireDay(int year, int month, int day)
   {
      Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
      
      // example of instance field mutation
      hireDay.setTime(newHireDay.getTime());
   }

   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }

   public String toString()
   {
      return "Employee[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
   }
}

相关文章

JOL:Java 对象内存布局_java内部对象

如果想要深入的学习 synchronized 关键字,必须提前掌握的一部分知识就是 Java 对象内存布局。通过这篇文章一起探索 Java 对象在虚拟机中是如何保存的。在正式学习后续内容之前,先约定如...

深圳尚学堂:干货来啦!JAVA常用代码(一)

1.获取环境变量System.getenv("PATH");System.getenv("JAVA_HOME");//2.获取系统属性System.getProperty("pencil color"...

JVM对象的创建过程_java创建对象的语句

对象的创建过程new对象image-202302261445419191:首先判断这个类有没有加载过,没有加载过的先加载到我们JVM内存中。2:分配内存指针碰撞:默认使用,如果JVM堆中内存绝对规整,...

JVM的内存模型详解_jvm内存模型及作用

总体架构图中,JVM内存模型主要包括三个系统:类加载系统:负责加载Java类的字节码文件,并将其转换为JVM内部的数据结构。运行时数据区:用于存储JVM运行时需要的各种数据方法区:存储类的结构信息(如...

Java程序员必备技能《上》_java程序员岗位职责

最近,有不少朋友来找我聊,作为java开发者,到底需要掌握哪些技能才能在这行业混下去,不至于找不到工作。可怜的Java开发者,你要学的东西实在是太多了,但是,也不必恐慌,毕竟很多知识可以等你找到一个工...

一天一个设计模式(五):建造者模式,构建优美的Java对象

建造者模式(Builder Pattern)是一种创建型设计模式,其主要作用是将一个复杂对象的构建过程与它的表现分离开。这种分离意味着同样的构建过程可以创建不同的表现,同时,不同的构建过程也可以用来创...