设计模式之原型模式

原型模式的介绍和定义

原型模式是一个创建型的模式。原型二字表明了改模式应该有一个样板实例,用户从这个样板对象中复制一个内部属性一致的对象,这个过程也就是我们称的“克隆”。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。

原型模式的定义如下:
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

原型模式的结构如下图所示:
在这里插入图片描述

JDK中Clonable和clone方法

java中在Object类中定义了clone方法,用于对象的拷贝,如下:

protected native Object clone() throws CloneNotSupportedException;

但是可以拷贝的前提是类必须实现Clonable接口:

public interface Cloneable {
}

就像序列化一样,类示例可以被序列化那么必须实现Serilizable接口,而对象可以被克隆拷贝则需要实现Clonable接口。当不实现Clonable接口,调用clone方法时会抛出CloneNotSupportedException异常。

浅拷贝与深拷贝

首先来看一段代码:

class Cat {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

class Dog {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

class Person implements Cloneable{
    private String name;
    private int age;
    private Cat cat;
    private Dog dog;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public Cat getCat() {
        return cat;
    }

    public void setCat(Cat cat) {
        this.cat = cat;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }

    @Override
    protected Object clone()  {
        Person person = null;
        try {
            person = (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return  person;
    }

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

对应的测试代码如下:

public static void main(String[] args) {
    Cat tom = new Cat();
    tom.setName("Tom");
    tom.setAge(8);

    Dog speike = new Dog();
    speike.setName("Speike");
    speike.setAge(10);

    Person master = new Person();
    master.setName("Frank");
    master.setAge(58);
    master.setCat(tom);
    master.setDog(speike);

    Person clone = (Person) master.clone();

    System.out.println(master == clone);
    System.out.println(master.getCat() == clone.getCat());
    System.out.println(master.getDog() == clone.getDog());
}

测试结果如下:

false
true
true

可以看到,经过拷贝后对象内部引用对象仍然是原先的引用对象,而被拷贝对象本身是一个新的对象,所以说明JDK的clone就是一个浅拷贝,对于基本类型数据是完全不同的,但是对于引用对象则只是拷贝了对象的引用。那么如何实现一个深拷贝呢?也就是说被拷贝的对象的引用属性也是被拷贝的,与原先的对象是不同的。针对上面的实例,我们需要将Cat类和Dog也实现Clonable接口并重写clone方法,修改如下:

class Cat implements Cloneable {
   
    // 省略。。。
   
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Dog implements Cloneable {

    // 省略。。。

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Person implements Cloneable{
  
    // 省略。。。
    
    @Override
    protected Object clone()  {
        Person person = null;
        try {
            person = (Person) super.clone();
            if (this.dog != null)
                person.dog = (Dog) this.dog.clone();
            
            if (this.cat != null)
                person.cat = (Cat) this.cat.clone();
            
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return  person;
    }

那么再次测试后结果如下:

false
false
false

可以看到,Person内部的引用类型属性也不同了,通过对内部的属性再次进行拷贝从而实现了深拷贝,当然如果Cat和Dog内部也存在引用类型数据,则也需要对其进行拷贝,但是是否会有一个疑问,就是为啥String为引用类型缺不需要拷贝,因为String的对象是不可变的,也就是在拷贝对象clone中修改name,但是在master中的name不会改变,所以就不需要对其进行处理了。

JDK中数组、集合及Map的处理

数组处理

首先来看数组,数据在进行拷贝时,会创建将一个新的对象,但是数据中的元素还是原先的一一对应,看以下一个示例代码:

Person[] src = new Person[3];
src[0] = new Person();
src[0] = new Person();
src[0] = new Person();

Person[] clone = src.clone();
for (int i = 0; i < src.length; i++) {
    System.out.println(src[i] == clone[i]);
}

结果如下:

true
true
true

可以看到,即使Person实现了Clonable接口并重写了clone方法,但是Person数组进行拷贝后还是被拷贝数据的原先元素。

集合处理

看一下ArrayList和LinkedList的继承关系:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable 

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

可以看到ArrayList和LinkedList均实现了Clonable接口,并且重写了clone方法:
LinkedList的clone方法:

private LinkedList<E> superClone() {
    try {
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}
public Object clone() {
    LinkedList<E> clone = superClone();

    // Put clone into "virgin" state
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    // Initialize clone with our elements
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

LinkedList的克隆就是新建一个LinekList对象,然后将被拷贝的所有元素重新添加一次。
ArrayList的clone方法如下:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

可以看到,ArrayList的处理与LinkedList差别不大,也是将原先的元素添加到新拷贝的对象中。
对于集合中来看一下HashSet:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

其clone方法如下:

public Object clone() {
    try {
        HashSet<E> newSet = (HashSet<E>) super.clone();
        newSet.map = (HashMap<E, Object>) map.clone();
        return newSet;
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}

可以看到是调用了Object方法创建一个新的对象,然后调用HashMap的clone方法。这就是HashSet的clone方法。紧接着看一下HashMap:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

其clone方法:

public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

在该方法中通过将super,clone创建一个拷贝对象,然后将重新初始化,也就是将所有的状态变为新建时的状态,最后通过putMapEntries方法将被拷贝对象的所有元素赋值到新拷贝的独享,如下:

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

原型模式的注意点和优缺点

原型模式的注意点

  1. 原型模式在拷贝时是不会调用类的构造方法的。
  2. 在使用原型模式时尽量使用深拷贝,防止浅拷贝导致一个对象被对个其他对象应用,那么一个对象修改数据时会影响到其他对象,或者将类设计成不可变的。
  3. 要使用clone方法,类的成员变量上不要增加final关键字。因为final类型是不允许重赋值的。

原型模式的优缺点

原型模式的优点:

  1. 原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可能更好的体现其优点。
  2. 还有一个重要的用途就是保护性拷贝,也就是对某个对象对外可能是只读的,为了防止外部对这个只读对象的修改,通常可以通过返回一个对象拷贝的形式实现只读的限制。

原型模式的缺点:

  1. 这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发中应该注意这个潜在问题。优点是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。
  2. 通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能够获得效率上的提升。

使用序列化处理深拷贝

当对象的结果非常的复杂,同时引用中又嵌套引用,那么使用深拷贝就会导致代码很复杂,每个类都需要实现Clonable接口并且重写clone方法。对于特别复杂的类需要实现深拷贝可以考虑使用序列化来完成深拷贝,当然每个参与类均需要实现Serializable接口,下面是Person类使用序列化来完成深拷贝的过程:

@Override
public Object clone() throws CloneNotSupportedException {
    Person clone = null;
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
        oos = new ObjectOutputStream(bout);
        oos.writeObject(this);
        oos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }

    ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
    try {
        ois = new ObjectInputStream(bin);
        clone = (Person) ois.readObject();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    try {
        oos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        ois.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return clone;
}

通过使用序列化从实现了深拷贝,但是通过代码我们可以知道,其是比较吃内存的,当大量的需要进行拷贝时,会大量的占用内存,降低性能。

1) 优秀的程序应该是这样的:阅读时,感觉很优雅;新增功能时,感觉很轻松;运行时,感觉很快速,这就需要设计模式支撑。 2) 设计模式包含了大量的编程思想,讲授和真正掌握并不容易,网上的设计模式课程不少,大多讲解的比较晦涩,没有真实的应用场景和框架源码支撑,学习后,只知其形,不知其神。就会造成这样结果: 知道各种设计模式,但是不知道怎么使用到真实项目。本课程针对上述问题,有针对性的进行了升级 (1) 授课方式采用 图解+框架源码分析的方式,让课程生动有趣好理解 (2) 系统全面的讲解了设计模式,包括 设计模式七大原则、UML类图-类的六大关系、23种设计模式及其分类,比如 单例模式的8种实现方式、工厂模式的3种实现方式、适配器模式的3种实现、代理模式的3种方式、深拷贝等 3) 如果你想写出规范、漂亮的程序,就花时间来学习下设计模式吧 课程内容和目标 本课程是使用Java来讲解设计模式,考虑到设计模式比较抽象,授课采用 图解+框架源码分析的方式 1) 内容包括: 设计模式七大原则(单一职责、接口隔离、依赖倒转、里氏替换、开闭原则、迪米特法则、合成复用)、UML类图(类的依赖、泛化和实现、类的关联、聚合和组合) 23种设计模式包括:创建型模式:单例模式(8种实现)、抽象工厂模式原型模式、建造者模式、工厂模式。结构型模式:适配器模式(3种实现)、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式(3种实现)。行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式) 2) 学习目标:通过学习,学员能掌握主流设计模式,规范编程风格,提高优化程序结构和效率的能力。
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页