面向对象设计模式--原型模式详解+实际应用(Java)

2023-03-23 05:14:24 浏览数 (1)

原型模式

原型模式解决从1到N个对象的生成,不负责生成第一个对象实例。原型模式可以通过直接复制内存的方式生成一个新的对象实例,与原有的对象实例的内容都相同,它省去了通过构造函数生成对象实例的步骤,省去了每个属性的赋值逻辑。如果构造函数中没有任何逻辑,则new方法要比clone方法快;但是,只要构造函数中有一点点逻辑,则clone方法就要比new快很多了,而且还没有考虑对象的内部属性进行赋值的逻辑时间。

应用场景

  • 对象之间相同或相似,即只是个别的几个属性不同的时候。
  • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
  • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。
  • 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。
优点
  • Java自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更加优良。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点
  • 需要为每一个类都配置一个clone方法。
  • clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
  • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

Java中的深拷贝与浅拷贝

浅拷贝

浅拷贝是将对象的栈上的属性直接拷贝一份给新对象,基本类型是没有问题的,但引用类型会拷贝一个地址引用,本质使用的还是堆上的同一个对象,修改时会同时发生变化。浅拷贝需要实现 Cloneable接口,不然无法调用clone方法,返回的是Object对象,可在重写中修改返回类型

代码语言:javascript复制
public class User implements Cloneable{
​
    // 基本类型
    private String name;
    private String email;
    
    // 引用类型
    private Date birthday;
​
    // 可重写,也可不写
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
​
    public static void main(String[] args) throws CloneNotSupportedException {
        User prototype = new User("oldHowl","old@qq.com",new Date());
        User shallowUser  = (User) prototype.clone();
​
        prototype.setName("newHowl");
        prototype.setEmail("new@qq.com");
        prototype.getBirthday().setMonth(10);
        
        System.out.println("prototype: "   prototype);
        System.out.println("shallowUser"   shallowUser);
    }
}
代码语言:javascript复制
// 修改原对象的基本类型的属性是不会改变克隆之后的对象属性
// 修改引用类型,公用一个堆上的引用对象,那么克隆对象也会被修改,解决方法是使用深拷贝,月份变成了10月
prototype: User{name='newHowl', email='new@qq.com', birthday=Tue Nov 02 14:29:35}
shallowUser: User{name='oldHowl', email='old@qq.com', birthday=Tue Nov 02 14:29:35}
深拷贝

对具有引用类型属性的对象进行copy,引用对象需要不是直接复制一个引用地址了,而是新建一个引用对象,这个需要手动重写clone方法

代码语言:javascript复制
public class User implements Cloneable {
​
    // 必须重写
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 对基本属性进行拷贝
        User deepClone = (User) super.clone();
        // 引用类型进行深拷贝
        deepClone.setBirthday((Date) deepClone.getBirthday().clone());
        return deepClone;
    }
​
    public static void main(String[] args) throws CloneNotSupportedException {
        
        User prototype = new User("oldHowl","old@qq.com",new Date());
        User shallowUser  = (User) prototype.clone();
​
        prototype.setName("newHowl");
        prototype.setEmail("new@qq.com");
        prototype.getBirthday().setMonth(10);
        
        System.out.println("prototype: "   prototype);
        System.out.println("shallowUser"   shallowUser);
    }
}
代码语言:javascript复制
// 引用类型的月份没有改变了,证明引用对象也是一个新的对象
prototype: User{name='newHowl', email='new@qq.com', birthday=Tue Nov 02 14:51:14}
shallowUserUser{name='oldHowl', email='old@qq.com', birthday=Wed Jun 02 14:51:14}

Java的所有类都是从java.lang.Object类继承而来的,而Object类提供protected Object clone()方法对对象进行复制,但Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用类型对象等都不会拷贝,这就是所谓浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。

使用序列化实现深拷贝

代码语言:javascript复制
public static Object deepClone(Object obj) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(obj);
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    return ois.readObject();
}

使用BeanUtils实现深拷贝

代码语言:javascript复制
public static Object deepClone(Object obj) throws IllegalAccessException, InstantiationException {
    Object clone = obj.getClass().newInstance();
    BeanUtils.copyProperties(obj, clone);
    return clone;
}

使用JSON实现深拷贝

代码语言:javascript复制
public static Object deepClone(Object obj) {
    String json = JSON.toJSONString(obj);
    return JSON.parseObject(json, obj.getClass());
}
利用序列化实现深拷贝实例:

在Java语言里深度复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的拷贝)写到一个流里(序列化),再从流里读回来(反序列化),便可以重建对象。把对象写到流里的过程是序列化(Serialization)过程;而把对象从流中读出来的过程则叫反序列化(Deserialization)过程。应当指出的是,写到流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。

先创建一个Person类,该类需实现Serializable接口,为测试其是否为深拷贝为其添加一个List容器属性family,代码如下。

代码语言:javascript复制
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
@SuppressWarnings("serial")
public class Person implements Serializable{
    // 姓名
    private String name;
    // 家庭成员
    private List<String> family;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<String> getFamily() {
        return family;
    }
    public void setFamily(List<String> family) {
        this.family = family;
    }
    public Person serializationClone() throws IOException,
            ClassNotFoundException {
        // 将对象写到流里
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 从流里读回来
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (Person) ois.readObject();
    }
}

创建一个客户端类来测试。

代码语言:javascript复制
import java.util.ArrayList;
import java.util.List;
public class MainClass {
    public static void main(String[] args) throws Exception {
        /**
         * 创建一个原型实例对象,以后复制它
         */
        Person person = new Person();
        /**
         * 为原型实例对象起个名字
         */
        person.setName("demo");
        /**
         * 给原型实例对象添加好家人,以测试serializationClone()方法是否对引用类型对象进行复制
         */
        List<String> family = new ArrayList<String>();
        family.add("wife");
        family.add("child");
        person.setFamily(family);
        /**
         * 使用serializationClone()方法进行复制
         */
        Person copyPerson=person.serializationClone();
        /**
         * 查看复制好的对象是否和原型实例相同
         */
        System.out.println("原型实例的名字:" person.getName());
        System.out.println("原型实例的家人:" person.getFamily());
        System.out.println("复制对象的名字:" copyPerson.getName());
        System.out.println("复制对象的家人:" copyPerson.getFamily());
        /**
         * 对复制好的对象属性进行修改,进一步测试该对象是否是真正的备份
         */
        copyPerson.setName("copy-demo");
        List<String> copyFamily = new ArrayList<String>();
        copyFamily.add("copy-wife");
        copyFamily.add("copy-child");
        copyPerson.setFamily(copyFamily);
        /**
         * 打印修改后的复制对象和原型对象
         */
        System.out.println("复制对象修改后的名字:" copyPerson.getName());
        System.out.println("复制对象修改后的家人:" copyPerson.getFamily());
        System.out.println("原型实例的名字:" person.getName());
        System.out.println("原型实例的家人:" person.getFamily());
    }
}

运行程序打印结果如下:

代码语言:javascript复制
原型实例的名字:demo
原型实例的家人:[wife, child]
复制对象的名字:demo
复制对象的家人:[wife, child]
复制对象修改后的名字:copy-demo
复制对象修改后的家人:[copy-wife, copy-child]
原型实例的名字:demo
原型实例的家人:[wife, child]

大功告成,person原型对象被完美深度复制了。

这样做的前提就是对象以及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象可否设成transient,从而将之排除在复制过程之外。

浅拷贝显然比深拷贝更容易实现,因为Java语言的所有类都会继承一个clone()方法,而这个clone()方法所做的正是浅拷贝。

有一些对象,比如线程(Thread)对象或Socket对象,是不能简单复制或共享的。不管是使用浅度克隆还是深度克隆,只要涉及这样的间接对象,就必须把间接对象设成transient而不予复制;或者由程序自行创建出相当的同种对象,权且当做复制件使用。

注意事项:
  • Java语言提供的Cloneable接口只起一个作用,就是在运行时期通知Java虚拟机可以安全地在这个类上使用clone()方法。通过调用这个clone()方法可以得到一个对象的复制。由于Object类本身并不实现Cloneable接口,因此如果所考虑的类没有实现Cloneable接口时,调用clone()方法会抛出CloneNotSupportedException异常。
  • 使用原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用Object类的clone方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。还记得单例模式吗?单例模式中,只要将构造方法的访问权限设置为private型,就可以实现单例。但是clone方法直接无视构造方法的权限,所以,单例模式与原型模式是冲突的,在使用时要特别注意。

原型管理器

原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器PrototypeManager类。该类用HashMap保存多个复制的原型,访问类可以通过管理器的get(String id)方法中获取复制的原型。在我们实际工作过程中,不推荐参照示例代码直接使用原型模式,而是建议使用管理器的方式实现原型模式

代码语言:javascript复制
public class PrototypeManager {
    private static Map<String, Prototype> map = new HashMap<>();
​
    static {
        map.put("prototype1", new ConcretePrototype1());
        map.put("prototype2", new ConcretePrototype2());
    }
​
​
    public static Prototype getPrototype(String name) {
        return map.get(name).clone();
    }
}
​
​
public interface Prototype {
    Prototype clone();
}
​
​
public class ConcretePrototype1 implements Prototype {
    @Override
    public Prototype clone() {
        return new ConcretePrototype1();
    }
}
​
​
public class ConcretePrototype2 implements Prototype {
    @Override
    public Prototype clone() {
        return new ConcretePrototype2();
    }
}

以上代码实现了一个原型管理器,其中包含两个具体原型类ConcretePrototype1和ConcretePrototype2,它们都实现了Prototype接口中的clone方法。在原型管理器中,我们使用一个Map来存储原型对象,通过getPrototype方法可以获取指定名称的原型对象的克隆对象。

原型模式在JDK源码中的应用

java.util.ArrayList 类中的 clone() 方法。该方法允许客户端通过复制现有列表来创建新列表,而无需了解如何创建该列表。

代码语言:javascript复制
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);
    }
}

java.util.HashMap 类中的 clone() 方法。该方法允许客户端通过复制现有映射表来创建新映射表,而无需了解如何创建该映射表。

代码语言:javascript复制
public Object clone() {
    HashMap<?,?> m;
    try {
        m = (HashMap<?,?>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
    m.reinitialize();
    m.putMapEntries(this, false);
    return m;
}
Date(深拷贝)

java.util.Date实现了Cloneable接口,重写了clone方法,并且调用了属性的clone方法。

代码语言:javascript复制
public Object clone() {
    Date d = null;
    try {
        d = (Date)super.clone();
        if (cdate != null) {
            d.cdate = (BaseCalendar.Date) cdate.clone();
        }
    } catch (CloneNotSupportedException e) {} // Won't happen
    return d;
}

原型模式在Spring源码中的应用

在Spring Boot中,Bean的作用域(Scope)是指Bean的生命周期和可见范围。Spring Boot遵循Spring的作用域规则,提供了多种作用域,包括Singleton、Prototype、Request、Session、Global Session等。其中,Singleton是Spring Boot默认的作用域,它表示一个Bean在整个应用程序中只有一个实例;而Prototype则表示每次请求都会创建一个新的Bean实例。

在Spring Boot中,Prototype作用域的Bean的实现与Spring相同,只需要在Bean的定义中设置scope属性为"prototype"即可。注意在@Scope注解上面声明开启作用域代理。

代码语言:javascript复制
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
    // ...
}

简单场景

在总商品池固定情况下,生成大量商品盲盒对象

代码语言:javascript复制
// 盲盒类
@Data
public class BlindBox implements Cloneable {
​
    private ArrayList<Item> blindItems = new ArrayList<>();
​
    public BlindBox() {
        Random random = new Random();
        for (int j = 0; j < 5; j  ) {
            int index = random.nextInt(ItemManager.getItems().size());
            blindItems.add(j, ItemManager.getItems().get(index));
        }
    }
​
    public BlindBox clone() throws CloneNotSupportedException {
        BlindBox cloned = (BlindBox) super.clone();
        cloned.blindItems = (ArrayList<Item>) blindItems.clone();
        Random random = new Random();
        for (int j = 0; j < 5; j  ) {
            int index = random.nextInt(ItemManager.getItems().size());
            cloned.blindItems.set(j, ItemManager.getItems().get(index));
        }
        return cloned;
    }
}
代码语言:javascript复制
// 商品类
@Data
public class Item {
    private String name;
​
    public Item(String name) {
        this.name = name;
    }
​
}
代码语言:javascript复制
// 总商品池类
@Data
public class ItemManager {
    private static ArrayList<Item> items = new ArrayList<>();
​
    static {
        for (int i = 1; i <= 20; i  ) {
            items.add(new Item("商品"   i   "号"));
        }
    }
​
    public static ArrayList<Item> getItems() {
        return items;
    }
}
代码语言:javascript复制
@Test
void blindBoxTest() throws CloneNotSupportedException {
    BlindBox box = new BlindBox();
    for (int i = 0; i < 1000; i  ) {
        BlindBox box2 = (BlindBox) box.clone();
        System.out.println("Blind box "   (i   1)   ": "   box2.getBlindItems().toString()   System.identityHashCode(box2.getBlindItems()));
    }
}

打印结果:

代码语言:javascript复制
Blind box 1: [Item(name=商品11号), Item(name=商品20号), Item(name=商品14号), Item(name=商品12号), Item(name=商品12号)]1407795127
Blind box 2: [Item(name=商品20号), Item(name=商品19号), Item(name=商品20号), Item(name=商品1号), Item(name=商品18号)]981865495
Blind box 3: [Item(name=商品6号), Item(name=商品11号), Item(name=商品12号), Item(name=商品4号), Item(name=商品20号)]589699084
Blind box 4: [Item(name=商品19号), Item(name=商品1号), Item(name=商品16号), Item(name=商品7号), Item(name=商品9号)]109987815
Blind box 5: [Item(name=商品1号), Item(name=商品3号), Item(name=商品1号), Item(name=商品17号), Item(name=商品20号)]469816326

注意复制不仅包括对象本身,也包括对象内集合:cloned.blindItems = (ArrayList<Item>) blindItems.clone();,只有这样的复制才能确保在操作复制对象时不影响原对象,如果删去上述代码,打印结果显示对象内集合的内存地址是相同的。

代码语言:javascript复制
Blind box 1: [Item(name=商品15号), Item(name=商品20号), Item(name=商品7号), Item(name=商品9号), Item(name=商品13号)]145494758
Blind box 2: [Item(name=商品9号), Item(name=商品13号), Item(name=商品1号), Item(name=商品17号), Item(name=商品12号)]145494758
Blind box 3: [Item(name=商品12号), Item(name=商品14号), Item(name=商品7号), Item(name=商品3号), Item(name=商品19号)]145494758
Blind box 4: [Item(name=商品6号), Item(name=商品4号), Item(name=商品10号), Item(name=商品18号), Item(name=商品16号)]145494758
Blind box 5: [Item(name=商品4号), Item(name=商品13号), Item(name=商品18号), Item(name=商品20号), Item(name=商品17号)]145494758

0 人点赞