克隆一个对象——原型模式深入解析

原型模式也是创建型的设计模式,字面意思其实很简单,就是复制一个对象,这里面有什么学问呢?

用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。

按照惯例,先讲故事。

我们都知道苹果有刻字服务,也就是假如你买了一款iPhone手机,你可以花一点钱让厂商给你刻上你想刻的字,这样体现了这款产品的独一无二性,很有意思。

那么现在,有甲乙丙都来买iPhone手机,并且都想刻上自己的名字,我们假设新手机是完全一模一样的。

常规的解决办法:

public class Apple
{
    private String characterCarve;
    private Product product;

    public String getCharacterCarve() {
        return characterCarve;
    }

    public void setCharacterCarve(String characterCarve) {
        this.characterCarve = characterCarve;
    }

    public Product getProduct() {
        return product;
    }

    public void setProduct(Product product) {
        this.product = product;
    }
    
    public Apple clone() {
        Apple prototype = new Apple();
        prototype.setCharacterCarve(this.characterCarve);
        prototype.setProduct(this.product);
        return prototype;
    }
}
public static void main(String[] args) {
    Apple one = new Apple();
    Product product = new Product();
    product.setProductName("iPhone");
    one.setCharacterCarve("甲");
    one.setProduct(product);
    Apple oneClone = one.clone();
    //这时候乙再刻上自己的名字
    oneClone.setCharacterCarve("乙");
    System.out.println(one.getCharacterCarve()+"/"+one.getProduct().getProductName());
    System.out.println(oneClone.getCharacterCarve()+"/"+oneClone.getProduct().getProductName());
    System.out.println(one.equals(oneClone));
}

输出结果为:

甲/iPhone
乙/iPhone
false

通过结果可以看出,我们的方法是有效的。甲买了一部iPhone并刻上了自己的名字“甲”,乙看到了以后也买了一部一模一样的,然后把名字改成自己的“乙”,对象one和它的克隆对象oneClone并不是同一个对象,但oneClone确实内部属性值与one相同。

在以上clone方法体中,我们手动将属性值重新set进clone出来的对象中,但是如果对象的属性特别多怎么办,我们要一个一个手动去set吗?

Java不必这么做

java的java.lang.Object类中提供了protected Object clone()方法,而我们都知道Java中所有的类都是继承自Object类,所以默认所有的类都可以使用该方法。

java使用clone()的方法和约束

  1. 克隆的类要实现Cloneable接口。
  2. 返回super.clone()
public class Apple implements Cloneable {

下面是重写clone方法时,IDE默认生成的代码。

@Override
protected Object clone() throws CloneNotSupportedException {
    // TODO Auto-generated method stub
    return super.clone();
}

重写它

protected Apple clone() {// 克隆方法
    try {
        return (Apple) super.clone();
    } catch (CloneNotSupportedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return null;
}
}

结果不发生改变。仍旧是

甲/iPhone
乙/iPhone
false

调用clone方法非常方便。

参考JDK API对clone()方法的解释:

  1. x.clone() != x 对象和克隆的对象不是同一个对象
  2. x.clone().getClass() == x.getClass() 对象和克隆的对象属于一个类
  3. x.clone().equals(x) 重写.equals方法,可以实现equals方法返回true

浅克隆和深克隆

我的前面的博文提到过java的值类型和引用类型的区别。

  • 浅克隆就是只将值类型的变量复制过去,而引用类型只是复制过去引用。
  • 深克隆是把值类型和引用类型的变量均开辟一块新的内存区域复制一份出来。

深克隆以后的对象与原对象是完全复制的却又独立的两个对象,而浅克隆以后的对象在引用变量上面,两个对象指向的仍旧是同一个内存地址。由此可以看出,如果使用浅克隆,原对象的引用类型的变量发生改变的时候,克隆对象也发生变化,这绝不是我们想要的结果。

上面写到的就是浅克隆代码,那么我们在代码中查看,浅克隆会出现什么问题呢?

product.setProductName("iWatch");//甲给厂商写信。厂商把产品换成了iWatch

乙在复制了甲以后,甲发现自己更想要iWatch,厂商把iPhone换成了iWatch,而此时乙并没有转变想法,看上去乙仍旧能够得到自己的iPhone,然而事实:

甲/iWatch
乙/iWatch
false

乙也变成了iWatch。

如果想让乙复制完甲以后,当原来的甲的产品发生更改时,乙并不受影响,这就要使用深复制。

那么如何保证每次我们都使用的是深克隆呢?

那就要让类去实现java.io.Serializable接口,而不是Cloneable接口了。

public class Product implements Serializable {

让成员对象也改为序列化,然后将原对象类改为序列化

public class Apple implements Serializable {

然后不去重写clone()方法了,自定义方法deepClone()

protected Apple deepClone() {// 深克隆方法
    try {
        // 将对象写入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);
    
        // 将对象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (Apple) ois.readObject();
    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return null;
    }

上面这段代码就是深克隆中标准的流的读取方式了,我们只要修改返回时的对象转型就好了。下面再来main方法中执行一下试一试,看看乙能否获得自己想要的iPhone吗。

甲/iWatch
乙/iPhone
false

太好了,甲虽然换了产品,但是克隆过来的乙并没有受到影响,他仍然可以拿到自己的iPhone。

Cloneable接口和Serializable接口都是空接口,这种空接口也称为标识接口z只是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

原型管理器

原型管理器就是定义一个新的管理器类专门负责这个原对象类的克隆工作,这个管理器要保证单例(一般工具类都要保证单例,以避免多线程冲突),对外提供一个克隆访问点。

package test;

import java.util.Hashtable;

public class AppleManager {
    // 定义一个Hashtable,用于存储原型对象
    private Hashtable ht = new Hashtable();
    private static AppleManager pm = new AppleManager();

    // 为Hashtable增加产品对象
    private AppleManager() {
        //创建iPhone的苹果产品
        Apple iphone = new Apple();
        iphone.setProduct(new Product("iPhone"));
        //创建iWatch的苹果产品
        Apple iWatch = new Apple();
        iWatch.setProduct(new Product("iWatch"));
        //将这两种产品在原型管理器初始化时保存在缓存中座位源对象,以供有对象前来克隆。
        ht.put("iPhone", iphone);
        ht.put("iWatch", iWatch);
    }

    // 增加新的产品对象
    public void addApple(String key, Apple Apple) {
        ht.put(key, Apple);
    }

    // 通过深克隆获取新的产品对象
    public Apple getApple(String key) {
        return ((Apple) ht.get(key)).deepClone();
    }

    public static AppleManager getPrototypeManager() {
        return pm;
    }
}

然后在调用的时候只需要

// 获取管理器
AppleManager pm = AppleManager.getPrototypeManager();
Apple one, oneClone;
one = pm.getApple("iPhone");
oneClone = pm.getApple("iWatch");

这里最终的运行结果不变,仍旧是

甲/iPhone
乙/iWatch
false

我们可以看到,通过原型管理器,我们可以在管理器中预先将源对象创建好,并且对外提供获取克隆对象的方法,这里是通过字符串来获取相应的源对象的克隆,然后在程序使用的时候,直接调用管理器的方法进行克隆。

我们发现这个字符串参数是和源对象中产品的名称是一致的,如果我们系统比较复杂,将这些产品变成产品类的子类去细化,那么我想也可以通过反射机制去自动创建,每当字符串参数和类名相同的时候,我们都要想到反射

正所谓,反射反射,程序员的快乐!

注意

有些语言中的某些集合对象是具备clone和copy方法的,正好对应着浅克隆和深克隆两种方式,

clone方法一定就是浅克隆!只复制结构,并不完全开辟新内存去复制。
copy方法就是深克隆,是可以将所有数据复制一份过去的。

原型模式的适用场景

  • 创建新对象成本较大,新对象与原对象又很相似,原对象稍作修改即可用。
  • 有时候,复制一个对象要比构造函数方便得多。

缺陷:

  • 每个类都要有一个克隆方法
  • 深克隆时代码比较复杂,而且当存在类内部的嵌套调用时,实现就更加费劲。
posted @ 2017-09-12 23:48 一面千人 阅读(...) 评论(...) 编辑 收藏