02_oo_17_clone

第三十章 对象复制(Object Cloning)

1 简介

对象复制(Object Cloning)是一个重要的特性,也是一个复杂的过程。Java语言的设计者最初希望对象通过Object类提供的clone()成员方法复制出新的对象。因为clone()是成员方法,开发人员可以覆盖(Override) clone()以实现个性化的需求(Customization)。然而,由于Java语言设计上的失误,导致了这种对象复制机制饱受批评,甚至在一些代码库中,开发人员已不再使用clone()成员方法复制对象。

本章首先将逐一介绍对象复制的概念和使用细节。在理解了繁琐的细节之后,本章还会指出对象复制机制在设计上的一些问题,以及列举一些构造或者复制对象的替代方法。

2 Clone成员方法和Cloneable接口

在Java语言对象复制机制中,Object类提供了clone()成员方法。该方法是protected,因此,在类(或者基类、子类)的外部是不能直接使用该类的clone()方法的。Object类中的clone()方法(即:默认的clone()方法)实现了两个目的。其一,生成出一个与this对象相同类型的对象;其二,依次拷贝this对象中的成员变量的值至新的对象中。因此,clone()方法满足以下测试。

  1. clone()方法返回的是一个新的对象。因此:x.clone() != x (即它们的引用(Reference)是不同的)。
  2. clone()方法返回的是同一类型的对象。因此:x.clone().getClass() == x.getClass()。
  3. 默认情况下,新对象包含与this对象相同的属性值(Attribute Values)。因此:在默认场景下,x.clone().equals(x)。

在使用clone()方法之前,需要提供对象复制功能的类还需要实现Cloneable接口。如果类没有实现Cloneable接口,Object类的clone()方法会在运行时抛出CloneNotSupportedException异常。所有数组类型自动实现了Cloneable接口。

3 浅复制(Shallow Copy)和深度复制(Deep Copy)

浅复制(Shallow Copy)和深度复制(Deep Copy)是两种复制策略。浅复制是指在复制过程中,仅仅复制对象的值,而并不会复制被成员变量引用的对象。更具体的说,如果成员变量是基本数据类型(Primitive Data Type),浅复制会复制这些成员变量的数值。如果成员变量是引用类型(Reference),浅复制会复制这些引用的数值(类似于C/C++语言中复制指针的值)。因此,在复制完成后,新旧对象中引用类型的成员变量会指向相同的对象。

例如,给定以下的Car类,对象c1.brand和c2.brand引用的是同一个字符串对象。

// Brand类抽象轿车的品牌
public class Brand  implements Cloneable {
    public String name = null;
    public String description = null;
    
    public Brand(String name, String description) {
        this.name = name;
        this.description = description;
    }
    
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

// Car类抽象轿车
public class Car implements Cloneable {
    public int price;
    public Brand brand = null;
    
    public Car(int price, Brand brand) {
        this.price = price;
        this.brand = brand;
    }
    
    public static void main(String[] args) {
        try {
            Car c1 = new Car(10000, new Brand("Ford", ""));
            Car c2 = (Car)c1.clone(); //如果这是浅复制(Shallow Copy)
            System.out.println(c1.brand == c2.brand); // 输出 True
        } catch (CloneNotSupportedException ex) {
            System.out.println(ex.getMessage());
        }
    }
}

深度复制则执行“深度”复制,即对引用类型的成员变量,深度复制会复制一个新的对象,复制后的成员变量会指向这个新的对象。

public class Car implements CLoneable {
    public int price;
    public Brand brand = null;
    
    public Car(int price, Brand brand) {
        this.price = price;
        this.brand = brand;
    }
    
    public static void main(String[] args) {
        Car c1 = new Car(10000, "Ford");
        Car c2 = c1.clone(); //如果这是深度复制(Deep Copy)
        System.out.println(c1.brand == c2.brand); // 输出 False
    }
}

如图一的左侧示例所示,在浅复制过程结束后,c1.brand和c2.brand指向的是同一个Brand对象。而在右侧的深度拷贝示例中,在深度复制过程结束后,c1.brand和c2.brand指向的是两个不同的Brand对象;但这两个对象的内容相同。

图一 浅复制(Shallow Copy) vs. 深度复制(Deep Copy)

图一 浅复制(Shallow Copy) vs. 深度复制(Deep Copy)

4 子类对象的复制

当上述的复制机制应用于继承中时,对象复制的过程将会变得比较复杂。当使用默认clone()成员方法时,使用的是浅复制。如果在类中覆盖了clone()方法,则可实现深度复制或者自定义的复制过程。在这里,开发人员需要注意的是:

  1. 在自定义clone()方法中,开发人员需要使用super.clone()方法构造一个同类型的变量。因为只有Object.clone()方法才能做到,因此,在自定义的clone()方法中需要使用super.clone()逐级调用上一级的clone()方法,最后由Object.clone()构造新的对象。
  2. 因为有些类的成员是私有的(private),所以只有在该类中覆盖clone()方法才能复制private成员变量。但是,也可以利用Object.clone()方法浅复制私有成员变量。
  3. clone()方法返回的是同类型的对象,但是,clone()的方法接口返回的是Object的引用,所以需要直接将新对象转换成同一类型的引用变量。
  4. 在自定义的clone()方法中可能会出现异常或者错误。例如,某成员变量的类型未实现Cloneable接口,但是在代码中对其调用了clone()方法。或者,在其他处理中(例如:类型转换)也可能出现异常。所以,如果出现异常,clone()方法需要抛出CloneNotSupportedException异常。
  5. 在自定义clone()方法中,不能使用new Car()来代替super.clone(),因为,如果这样实现的话,在Car类的继承类中会出现问题。假如类CompactCar继承自类Car,在CompactCar类的clone()方法中需要返回CompactCar类型的对象,而不是Car类型的对象。因为Object.clone()方法是在Java虚拟机内部实现的(Native Codes),所以,该方法能够获得当前对象的类信息,以及复制其所有的成员变量。
  6. 在并行应用程序中,需要使用synchronize保护好对象复制的过程,以避免新变量在复制完成之前被另一个线程使用。

如下面的例子所示,Car类自定义了一个深度复制的clone()方法。

public class Car implements Cloneable {
    public int price;
    public Brand brand = null;
    
    public Object clone() throws CloneNotSupportedException {
      try {
          Car car = Car.class.cast(super.clone());
          // super.clone() 已经复制了price成员变量,所以不再需要给price赋值
          //car.price = this.price;  
          car.brand = Brand.class.cast(this.brand.clone());
          return car;
      } catch (CloneNotSupportedException ex) {
          throw ex;
      } catch (Exception ex) {
          throw new CloneNotSupportedException(ex.getMessage());
      }
    }
}

5 对象复制机制的批评

在理解了Java对象复制机制后,我们再来看看对其的批评,因为该机制的设计有着许多不合理的地方。

  1. Cloneable接口并没有声明任何函数。我们常称这种接口为标识接口(Marker Interface)。从该接口的定义和名称来看,实现了该接口的类都能使用clone()函数复制对象。然而,在面向对象的思想下,可拷贝的对象是可以使用Cloneable接口类型的引用变量引用的。但是,因为该接口没有声明任何函数,所以,该接口的引用变量是无法调用clone()方法的,从而违背了面向对象的编程思想。如下例所示,c1.clone()是错误的,因为Cloneable接口没有声明clone()方法。
Cloneable c1 = new Car();
Cloneable c2 = c1.clone(); //错误,Cloneable接口没有声明clone()方法。
  1. Clone()方法与Cloneable接口的分离造成了不必要的混淆。Clone()方法是在类Object中定义的,许多开发人员无法理解为什么是否能调用clone()方法是由一个完全独立的Cloneable接口决定的。
  2. clone()方法返回的是相应类的对象,然而,该方法声明中返回的类型是Object。因此,每当调用clone()方法后,开发人员必须使用类型转换,将其转换成相应类的对象。
  3. 在面向对象程序设计中,所有的对象都是需要由构造函数创建的。然而,clone()方法是一个例外。这为开发人员引来了许多不必要的麻烦,也违背了面向对象程序设计思想。
  4. 当在一个类中实现了自定义的clone()方法时,这往往意味着这个类中的成员变量不可定义为final,因为它们会在clone()方法中依次赋值。
  5. 当继承的层数较多时,自定义的clone()方法会变得非常复杂。

6 对象复制的其他途径

为了避免clone()方法带来的复杂性,许多开发人员选择了其他的方法构造对象。在这里,我们仅简单的介绍一下这些方法。详细的内容会在后续的章节中介绍。

  1. 拷贝构造函数(Copy Constructor)。Java支持拷贝构造函数,但是,与C++不同的是,Java编译器不会自动生成拷贝构造函数。需要注意的是,在子类的拷贝构造函数中需要使用super()调用父类的拷贝构造函数,以初始化父类中的成员变量。如下例所示,super(c)调用了父类Vehicle的拷贝构造函数。
public class Vehicle {
    public int age;
    public Vehicle(Vehicle v) {
        this.age = this.v;
    }
}

public class Car extends Vehicle {
    public int price;
    public Brand brand = null;
    
    public Car(Car c) {
      super(c); // 调用父类的拷贝构造函数,初始化父类中的成员变量
      this.price = c.price;
      this.brand = c.brand; // 这里可以选择浅拷贝或者深拷贝
    }
}
  1. 静态工厂方法(Static Factory Methods)。在类中定义静态工厂方法也是一种常见的构造对象的方式。如下例所示,在工厂函数中可以使用构造函数创建一个新的对象,并对其赋值。
public class Car {
    public int price;
    public Brand brand;
    
    public static Car make(Car c) {
      Car newCar = new Car();
      // 成员变量赋值
      newCar.price = c.price;
      newCar.brand = new Brand(c.brand); // 这里可以选择浅拷贝或者深拷贝
      return newCar;
    }
}
  1. Builder类。Builder类方法是在类中定义一个内部类,专门用于创建该类的对象。如下例所示,在Car类中的CarBuilder是一个辅助类,利用函数调用链(Method Chaining)来创建Car的对象。
public class Car {
    public int price;
    public Brand brand = null;
  
    Car(int price, Brand brand) {
      this.price = price;
      this.brand = brand;
    }
  
    // Car类的静态方法,用于构造一个CarBuilder对象
    public static CarBuilder builder() {
      return new CarBuilder();
    }
  
    // Car类的一个内部类,用于构造Car对象
    public static class CarBuilder {
        private int price;
        private Brand brand;
        
        // 默认构造函数,被Car.builder()静态方法用于创建CarBuilder对象
        CarBuilder() { 
        }
        
        // price() 和 brand() 成员方法用于保存price和brand的值
        // 返回this对象,使得调用者可以使用函数调用链。
        public CarBuilder price(int price) {
          this.price = price;
          return this;
        }
        
        public CarBuilder brand(Brand brand) {
          this.brand = brand;
          return this;
        }
    
        // 最后由build()成员函数构造Car类的对象
        public Car build() {
          return new Car(this.price, this.brand);
        }
    }

    public static void main(String[] args) {
        Car car = Car.builder().price(10000).brand(new Brand("Ford", "...")).build();
    }
}

7 结语

本章介绍了Java语言中的对象复制机制,以及一些替代方法。因为在clone()方法的设计上出现了较大的缺陷,开发人员逐渐的使用工厂类或者builder类来复制或者创建对象。因为是在Object类中提供的clone()方法,如果Java语言的设计者修改对象复制机制会造成严重的版本兼容性的问题,因此,clone()方法仍然保留至今。笔者也希望在未来的版本中,Java能较好的解决这个问题。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.