设计模式系列【15】:原型模式(原型设计模式)详解

原型模式是一种创建型设计模式,用于高效地创建对象,尤其适用于大量相似对象的创建。Java中通过实现Cloneable接口和重写clone()方法实现浅克隆和深克隆。深克隆能保存对象状态,辅助撤销操作。模式优点包括性能优良,但也存在需为每个类配置clone方法、违背开闭原则等缺点。应用场景包括对象创建成本高、需要保存对象状态等。Spring框架中的scope='prototype'和JSON.parseObject()都是原型模式的应用。在实际使用中,要注意浅克隆与深克隆的区别,以及如何通过扩展实现带原型管理器的原型模式。

在有些系统中,存在大量相同或相似对象的创建问题,如果传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成的对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出了很多孙悟空一样简单。

一、原型模式的定义与特点

原型(Prototype)模式是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供一种创建对象的最佳方式。用这种方式创建的对象非常高效,根本无须知道对象创建的细节。例如,Windows操作系统的安装通常较耗时,如果复制就快了很多。

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

原型模式的优点:

  • Java自带的原型模型基于内存二进制流的复制,在性能上比直接new一个对象更加优良;
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一刻状态),可辅助实现撤销操作。

原型模式的缺点:

  • 需要为每一个类都配置一个clone方法;
  • clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则;
  • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

原型模式的应用场景:

  • 对象之间相同或相似,即只是个别的几个属性不同的时候;
  • 创建对象成本大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源;
  • 创建一个对象需要频繁的数据准备或访问权限等,需要提高性能或者提高安全性;
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。

在Spirng中,原型模式应用的非常广泛,例如scope=‘prototype’、JSON.parseObject()等都是原型模式的具体应用。

二、原型模式的结构与实现

由于Java提供了对象的clone()方法,所以用Java实现原型模式很简单。

1.模式的结构

原型模式包含以下主要角色:

  1. 抽象原型类:规定了具体原型对象必须实现的接口;
  2. 具体原型类:实现抽象原型类的clone()方法,它是可被复制的对象;
  3. 访问类:使用具体原型类中的clone()方法来复制新的对象。

其结构如下如所示:
在这里插入图片描述
2.模式的实现:

原型模式的克隆分为浅克隆和深克隆,实现克隆的方式:

  1. 对象的类需要实现Cloneable接口
  2. 重写Object类中的clone()方法
  3. 根据重写的clone()方法得到想要的克隆结果,例如浅克隆与深克隆。

深克隆和浅克隆的区别:

  • 浅克隆:创建一个新对象,复制对象时仅仅复制对象本身,包括基本属性,但该对象的属性引用其他对象时,该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个(仍指向原有属性所指向的对象的内存地址);
  • 深克隆:创建一个新对象,复制对象本身的同时,也复制对象包含的引用指向的对象,即修改被克隆对象的任何属性都不会影响到克隆出来的对象(不再指向原有对象地址)。

注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现Cloneable,重写clone();深拷贝是通过实现 Serializable 读取二进制流。

在这里插入图片描述
在这里插入图片描述

三、原型模式的应用实例

【例1】拷贝基本类型的属性:

class Person implements Cloneable{

    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

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

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

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

    @Override
    protected Person clone() throws CloneNotSupportedException {  
        return (Person)super.clone();   //调用父类的clone方法
    }
}

测试代码:

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person(22,"LiLei");
        Person newPerson = person.clone();
        person.setAge(21);
        person.setName("HanMeimei");
        System.out.println(person.toString());
        System.out.println(newPerson.toString());
    }
}

测试结果:

Person{age=21, name='HanMeimei'}
Person{age=22, name='LiLei'}

结论: 即在克隆出新的对象后,修改被克隆对象的基本属性,并不会影响克隆出来的对象。但当被克隆的对象的属性引用其他对象时,此时会有不同的结果。

【例2】浅克隆:

/**
 * 学生类
 */
class Student implements Cloneable{
    private String name;
    private Achievement achievement; //成绩

    public Student(String name, Achievement achievement) {
        this.name = name;
        this.achievement = achievement;
    }

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

    public void setAchievement(Achievement achievement) {
        this.achievement = achievement;
    }

    public Achievement getAchievement() {
        return achievement;
    }

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

    @Override
    protected Student clone() throws CloneNotSupportedException {
       return (Student) super.clone(); 
    }
}
    
/**
 * 成绩类
 */
class Achievement implements Cloneable{
    private float Chinese;
    private float math;
    private float English;

    public Achievement(float chinese, float math, float english) {
        Chinese = chinese;
        this.math = math;
        English = english;
    }

    public void setChinese(float chinese) {
        Chinese = chinese;
    }

    public void setMath(float math) {
        this.math = math;
    }

    public void setEnglish(float english) {
        English = english;
    }

    @Override
    public String toString() {
        return "Achievement{" +
                "Chinese=" + Chinese +
                ", math=" + math +
                ", English=" + English +
                '}';
    }

    @Override
    protected Achievement clone() throws CloneNotSupportedException {
        return (Achievement) super.clone();
    }
}

测试代码:

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Achievement achievement = new Achievement(100,100,100);
        Student student = new Student("LiLei",achievement);
        // 克隆出一个对象
        Student newStudent = student.clone();

        // 修改原有对象的属性
        student.setName("HanMeimei");
        student.getAchievement().setChinese(90);
        student.getAchievement().setEnglish(90);
        student.getAchievement().setMath(90);

        System.out.println(newStudent);
        System.out.println(student);

    }
}

测试结果:

Student{name='LiLei', achievement=Achievement{Chinese=90.0, math=90.0, English=90.0}}
Student{name='HanMeimei', achievement=Achievement{Chinese=90.0, math=90.0, English=90.0}}

结论: 以上现象表明,上述克隆方式为浅克隆,并不会克隆对象的属性引用的对象,当修改被克隆对象的成绩时,克隆出来的对象也会跟着改变,即两个对象的属性引用指向的是同一个对象。

【例三】但只要修改一下Student类中重写的clone()方法,即可实现深克隆:

修改代码如下:

@Override
    protected Student clone() throws CloneNotSupportedException {
        Student student =  (Student) super.clone();
        Achievement achievement = student.getAchievement().clone();
        student.setAchievement(achievement);
        return student;
    }

测试结果:

Student{name='LiLei', achievement=Achievement{Chinese=100.0, math=100.0, English=100.0}}
Student{name='HanMeimei', achievement=Achievement{Chinese=90.0, math=90.0, English=90.0}}

即在Student类中的clone()方法中再克隆一次Achievement对象,并赋值给Student对象。

值得一提的是,上文所说的浅拷贝只会克隆基本数据属性,而不会克隆引用其他对象的属性,但String对象又不属于基本属性,这又是为什么呢?
这是因为String对象是不可修改的对象,每次修改其实都是新建一个新的对象,而不是在原有的对象上修改,所以当修改String属性时其实是新开辟一个空间存储String对象,并把引用指向该内存,而克隆出来的对象的String属性还是指向原有的内存地址,所以String对象在浅克隆中也表现得与基本属性一样。

四、原型模式的扩展

原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器PrototypeManager类。该类用HashMap保存多个复制的原型,Client类可以通过管理器的get(String id)方法从中获取复制的原型。其结构如下图所示:
在这里插入图片描述
【例】用带原型管理器的原型模式来生成包含“圆”和“正方形”等图形的原型,并计算其面积。分析:本实例中由于存在不同的图形类,例如,“圆”和“正方形”,它们计算面积的方法不一样,所以需要用一个原型管理器来管理它们,下图所示是其结构图:
在这里插入图片描述
程序代码如下:

import java.util.*;
interface Shape extends Cloneable {
    public Object clone();    //拷贝
    public void countArea();    //计算面积
}
class Circle implements Shape {
    public Object clone() {
        Circle w = null;
        try {
            w = (Circle) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println("拷贝圆失败!");
        }
        return w;
    }
    public void countArea() {
        int r = 0;
        System.out.print("这是一个圆,请输入圆的半径:");
        Scanner input = new Scanner(System.in);
        r = input.nextInt();
        System.out.println("该圆的面积=" + 3.1415 * r * r + "\n");
    }
}
class Square implements Shape {
    public Object clone() {
        Square b = null;
        try {
            b = (Square) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println("拷贝正方形失败!");
        }
        return b;
    }
    public void countArea() {
        int a = 0;
        System.out.print("这是一个正方形,请输入它的边长:");
        Scanner input = new Scanner(System.in);
        a = input.nextInt();
        System.out.println("该正方形的面积=" + a * a + "\n");
    }
}
class ProtoTypeManager {
    private HashMap<String, Shape> ht = new HashMap<String, Shape>();
    public ProtoTypeManager() {
        ht.put("Circle", new Circle());
        ht.put("Square", new Square());
    }
    public void addshape(String key, Shape obj) {
        ht.put(key, obj);
    }
    public Shape getShape(String key) {
        Shape temp = ht.get(key);
        return (Shape) temp.clone();
    }
}
public class ProtoTypeShape {
    public static void main(String[] args) {
        ProtoTypeManager pm = new ProtoTypeManager();
        Shape obj1 = (Circle) pm.getShape("Circle");
        obj1.countArea();
        Shape obj2 = (Shape) pm.getShape("Square");
        obj2.countArea();
    }
}

运行结果如下所示:

这是一个圆,请输入圆的半径:3
该圆的面积=28.2735

这是一个正方形,请输入它的边长:3
该正方形的面积=9
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你好像很好吃a

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

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

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

打赏作者

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

抵扣说明:

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

余额充值