一文学会如何使用原型模式

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

原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时时,可以利用系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。我们先来看一个简单的案例结构图。

在这里插入图片描述

从UML图中,我们可以看出主要包含三个角色:

  • 客户端

    客户端提出创建的请求

  • 抽象原型

    规定拷贝接口

  • 具体原型

    被拷贝的对象

应用场景

  • 类初始化消耗资源较多
  • new 产生的一个对象需要非常繁琐的过程(数据准备,访问权限等)
  • 构造函数比较复杂
  • 循环体中生产大量对象时

原型模式的通用写法

按照如上类图,一个标准的原型模式代码应该具有三部分。首先我们创建原型IPrototype接口:

public interface IPrototype<T> {

    T clone();

}

创建具体的需要克隆的对象ConcretePrototype

public class ConcretePrototype  implements IPrototype{

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public ConcretePrototype clone() {
        ConcretePrototype current = new ConcretePrototype();
        current.setAge(this.age);
        current.setName(this.name);
        return current;
    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

测试代码:

public class Main {

    public static void main(String[] args) {
        // 创建原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setName("CC");
        prototype.setAge(18);
        System.out.println(prototype);

        // 拷贝
        ConcretePrototype clone = prototype.clone();
        System.out.println(clone);
    }
    
}

以上便是一个最简单的原型模式。虽然在这个简单场景下,我们这样操作好像复杂了,但是如果有几百个属性需要复制,采用这个方法就比较方便了。但是,上面的复制过程是我们自己完成的,在实际编码中我们一般不会做这种体力劳动,一般我们会采用浅克隆,深克隆两种方法。

浅克隆

普通写法中,我们是自己定义的一个克隆接口,实际上JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。

我们来改造下ConcretePrototype类的代码:

public class ConcretePrototype  implements Cloneable {

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

这个时候重新测试,可以得到同样的结果。通过这个方式再多的属性赋值我们也能轻而易举的搞定了。

但是这种方法也会有点问题,我们再来测试一下,首先我们在原型类中添加要给爱好属性:

@Data
public class ConcretePrototype  implements Cloneable {

    private int age;

    private String name;

    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

}

修改客户端测试代码:

public class Main {

    public static void main(String[] args) {
        // 创建原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setName("CC");
        prototype.setAge(18);
        List<String> hobbies = new ArrayList<>();
        hobbies.add("书法");
        hobbies.add("音乐");
        prototype.setHobbies(hobbies);
        System.out.println(prototype);

        // 拷贝
        ConcretePrototype clone = prototype.clone();
        clone.getHobbies().add("游戏");
        System.out.println(clone); //克隆对象
        System.out.println(prototype); //原型对象
    }
}
ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐])
ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐, 游戏])
ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐, 游戏])

我们给克隆对象添加了一个爱好后,发现原型对象也发生了变化,这显然不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话如果我们修改任意一个对象的属性值,两个对象的值都会变化。这就是我们常说的浅克隆:只是完整复制了值类型数据,没有复制引用对象。这显然不是我们想要的结果,为了处理这个问题,我们需要使用到深克隆。

深克隆

深克隆我们这里主要使用序列化以及反序列化来实现,当然也可以使用其他方法,比如json的方式,通过json字符串将其转换为对象实习。

我们继续改造代码,增加一个deepClone方法。

由于使用序列化,所以我们需要实现Serializable接口。

@Data
public class ConcretePrototype  implements Cloneable,Serializable {

    private int age;

    private String name;

    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    public ConcretePrototype deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (ConcretePrototype) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

这个时候我们再去测试,发现结果是我们想要的了,两个对象互不影响。

如何解决克隆破坏单例模式

如果我们克隆的对象是单例对象,那意味着克隆就会破坏单例。实际上防止克隆破坏单例模式的解决思路非常简单,禁止克隆即可。要么我们直接单例类不实现Cloneable接口;要么我们重写clone方法,在clone方法中直接返回单例对象。

@Data
public class ConcretePrototype  implements Cloneable {

    private int age;

    private String name;

    private List<String> hobbies;

    private static ConcretePrototype instance = new ConcretePrototype();

    private static ConcretePrototype getInstance(){
        return instance;
    }

    @Override
    public ConcretePrototype clone() {
        return instance;
    }
    
}

优缺点

优点

  • 性能优良,基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并保存起来,简化了创建对象的过程,以便在需要的时候使用,类似撤销操作

缺点

  • 需要为每个类配置一个克隆方法
  • 克隆方法位于类的内部,当对已有类改造的时候需要又改代码,破坏了开闭原则。
  • 在实现深克隆时需要编写较复杂的代码,而且当对象之前存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来比较麻烦。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

、楽.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值