• 创建型设计模式 之 原型模式


    同为创建型模式的原型模式与单例模式是密不可分的,这也是最常用的设计模式之一。

    原型模式是一种非常简单的设计模式。这里除了基本介绍和演示,还详细介绍了Java中原型模式的本质。

    一、介绍

      同样,先来看一下《研磨设计模式》的定义——用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

      原型模式的本质——克隆生成对象。

      那么原型模式是什么意思呢?说白了就是克隆自身。我们知道Java中没有引用这个概念,Java用变量名代表引用。像 Apple a = new Apple();我们知道,想要操作这个Apple对象,那么就是去操作"a"这个变量名,实质就是去操作“a”这个引用所指向的内存地址上的Apple对象。通常而言,赋值操作符“=”的本质就是将“=”右边的地址赋值给左边的引用。如果我们希望创建一个跟这个对象a一样的Apple对象,同时在操作这个新对象的时候,对象a无任何影响。Java新手可能下意识觉得使用Apple b = a; 很明显a和b指向的是同一片内存空间。

      原型模式就是为了解决这样的对象复制的问题。

      Java中的原型模式实现起来其实很简单,在对象的接口中添加一个复制自身的抽象方法,然后对象实现这个方法,复制自身即可。使用的时候直接调用接口方法即可。

    二、我的实现

    1、我们有一个水果接口,如下:

    1 public interface Fruit {
    2 
    3     public double getPrice();
    4     
    5     public void setPrice(double price);
    6     
    7     //克隆接口
    8     public Fruit cloneFruit();
    9 }

    2、一个简单实现类:

     1 public class Apple implements Fruit,Cloneable {
     2 
     3     // 价格
     4     private double price;
     5     // 平均尺寸
     6     private double avgSize;
     7     // 产地
     8     private String productionArea;
     9 
    10     public Apple(double price, double avgSize, String productionArea) {
    11         super();
    12         this.price = price;
    13         this.avgSize = avgSize;
    14         this.productionArea = productionArea;
    15     }
    16 
    17     public double getAvgSize() {
    18         return avgSize;
    19     }
    20 
    21     public void setAvgSize(double avgSize) {
    22         this.avgSize = avgSize;
    23     }
    24 
    25     public String getProductionArea() {
    26         return productionArea;
    27     }
    28 
    29     public void setProductionArea(String productionArea) {
    30         this.productionArea = productionArea;
    31     }
    32 
    33     @Override
    34     public void setPrice(double price) {
    35         // TODO Auto-generated method stub
    36         this.price = price;
    37     }
    38 
    39     @Override
    40     public double getPrice() {
    41         // TODO Auto-generated method stub
    42         return price;
    43     }
    44 
    45     @Override
    46     public String toString() {
    47         return "Apple [avgSize=" + avgSize + ", price=" + price
    48                 + ", productionArea=" + productionArea + "]";
    49     }
    50 
    51     //克隆自身的实现
    52     @Override
    53     public Fruit cloneFruit()
    54     {
    55         return new Apple(price, avgSize, productionArea);
    56     }
    57 
    58 }

    3、然后就可以测试了,如下:

     1 package prototype.myPrototype;
     2 
     3 public class Client {
     4 
     5     public static void main(String[] args)
     6     {
     7         Fruit fruit = new Apple(1,2,"红富士");
     8         System.out.println(fruit);
     9         
    10         //根据原对象克隆
    11         Fruit cloneFruit = fruit.cloneFruit();
    12         System.out.println(cloneFruit);
    13         System.out.println("两个水果是同一个吗?"+(fruit==cloneFruit));
    14         
    15     }
    16 }

    4、结果如下:

    Apple [avgSize=2.0, price=1.0, productionArea=红富士]
    Apple [avgSize=2.0, price=1.0, productionArea=红富士]
    两个水果是同一个吗?false

    如上,简单的实现了原型模式。也实现了接口与实现分离的克隆。在完全不知道对象类型的情况下完成了复制。

    三、Java的原型模式(Object的clone()方法)

    (1)、介绍

      Object类有一个clone()方法,这是java为实现原型模式准备的。

      要实现Java的克隆方法,要满足三个条件:

      1、调用对象实现了cloneable接口

      2、调用对象重写了public Object clone();方法。

      3、重写clone()时,调用super.clone();

      我们来看一下clone方法的原型

      protected native java.lang.Object clone() throws java.lang.CloneNotSupportedException

      我们可以看到,这个方法是protected访问控制,同时是一个本地方法。也就是说,任何Object的子类都可以调用这个方法,但是Object对象自身不能调用这个方法。

      这个方法执行的时候,是使用RTTI(run-time type identification)的机制,动态得找到目前正在调用clone方法的那个reference,根据它的大小申请内存空间,然后进行bitwise的复制,将该对象的内存空间完全复制到新的空间中去,从而达到shallowcopy的目的。(这句话摘自百度知道)

    (2)、问题:

      1、为什么调用对象要实现cloneable接口?

      因为clone方法执行的时候,会判断当前对象是否实现了Cloneable这个标识接口,如果没有实现,就抛出CloneNotSupportedException异常。

      2、为什么调用对象要重写public Object clone();方法,为什么要调用super.clone()?

      如果调用对象不重写public Object clone()方法,那么clone()方法都不会显示出来。调用clone()方法,直接编译错误。可是clone()方法不是protected的吗?

      我们来看一下clone();方法的API:

    By convention, the returned object should be obtained by calling super.clone. If a class and all of its superclasses (except Object) obey this convention,
    it will be the case that x.clone().getClass() == x.getClass().
    --按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则 x.clone().getClass() == x.getClass()

      因为只有重写clone()方法,我们才能通过使用super.clone(),才能真正调用Object类的本地方法clone();

      从这里我们可以看到,clone()方法是很特殊的。不重写,会编译错误,应该涉及到编译器的优化。

    (3)、实现:  

    我们来Java的原型模式简单改一下上面的示例:

    1、将Fruit接口中的克隆功能去掉,这里不列出了。

    2、Apple类实现Fruit接口,也实现Cloneable接口,并重写clone()方法,如下:

     1 public class Apple implements Fruit, Cloneable {
     2 
     3     // 价格
     4     private double price;
     5     // 平均尺寸
     6     private double avgSize;
     7     // 产地
     8     private String productionArea;
     9 
    10     public Apple(double price, double avgSize, String productionArea)
    11     {
    12         super();
    13         this.price = price;
    14         this.avgSize = avgSize;
    15         this.productionArea = productionArea;
    16     }
    17 
    18     public double getAvgSize()
    19     {
    20         return avgSize;
    21     }
    22 
    23     public void setAvgSize(double avgSize)
    24     {
    25         this.avgSize = avgSize;
    26     }
    27 
    28     public String getProductionArea()
    29     {
    30         return productionArea;
    31     }
    32 
    33     public void setProductionArea(String productionArea)
    34     {
    35         this.productionArea = productionArea;
    36     }
    37 
    38     @Override
    39     public void setPrice(double price)
    40     {
    41         // TODO Auto-generated method stub
    42         this.price = price;
    43     }
    44 
    45     @Override
    46     public double getPrice()
    47     {
    48         // TODO Auto-generated method stub
    49         return price;
    50     }
    51 
    52     // 重写了Object类的clone()方法
    53     @Override
    54     public Object clone()
    55     {
    56         // TODO Auto-generated method stub
    57         Object cloneApple = null;
    58         // 直接调用父类的克隆方法即可
    59         try
    60         {
    61             cloneApple = super.clone();
    62         } catch (CloneNotSupportedException e)
    63         {
    64             e.printStackTrace();
    65         }
    66         return cloneApple;
    67     }
    68 
    69     @Override
    70     public String toString()
    71     {
    72         return "Apple [avgSize=" + avgSize + ", price=" + price + ", productionArea=" + productionArea + "]";
    73     }
    74 
    75 }

    3、测试一下:

     1 public class Client {
     2 
     3     static Fruit fruit = new Apple(90, 5, "新疆");
     4 
     5     public static void main(String[] args) {
     6         System.out.println("Fruit:" + fruit);
     7         Fruit newFruit = (Fruit) ((Apple)fruit).clone();
     8         System.out.println("new Fruit : " + newFruit);
     9         System.out.println("前后是否为同一对象?" + (fruit == newFruit));
    10     }
    11 }

    4、结果:

    Fruit:Apple [avgSize=5.0, price=90.0, productionArea=新疆]
    new Fruit : Apple [avgSize=5.0, price=90.0, productionArea=新疆]
    前后是否为同一对象?false

    个人认为,方便而言,还不如自己手动实现原型模式。

    由于Java的原型使用RTTI的机制,速度块,若需要克隆重量级或复杂的对象时适合使用。不过要注意的是,Java的原型模式属于浅度克隆。下面我们会讲如何使用Java的原型模式实现深度克隆。

    这里再补充一点:

    我们来看,static Fruit fruit = new Apple(90, 5, "新疆");

    对于对象fruit,它的编译时类型是Fruit,运行时类型是Apple,所以fruit对象只能显式调用Fruit接口所有的方法。而这其中包含了Object类除了clone()方法之外的所有方法。Java中,接口和类是一个并列的概念

    我们可以认为接口是一种特殊的类,它默认实现了Object中除了clone()之外的像toString()、getClass等一系列Object方法。

    四、深度克隆和浅度克隆

    原型模式中的拷贝分为"浅拷贝"和"深拷贝":
    浅拷贝: 对值类型的成员变量进行值的复制,对引用类型的成员变量只复制引用,不复制引用的对象.
    深拷贝: 对值类型的成员变量进行值的复制,对引用类型的成员变量也进行引用对象的复制.

      下面是Java克隆、深度克隆的简单示例:

    1、首先是一个包含刚才Fruit类的Businessman对象:

    @Data
    public class Businessman implements Cloneable {
    
    private String name;
    private Fruit fruit;
    
    public Businessman(String name, Fruit fruit)
    {
      super();
      this.name = name;
      this.fruit = fruit;
    }
    
    //克隆方法
    public Object clone(){
      //先克隆一下引用变量
      Fruit f = (Fruit) ((Apple)fruit).clone();
      Businessman man = new Businessman(name,f);
      return man;
     }
    }

    2、测试一下:

     1 package prototype;
     2 
     3 public class Client {
     4 
     5     public static void main(String[] args) {
     6         //水果类
     7         Fruit fruit = new Apple(90, 5, "新疆");
     8         //商人
     9         Businessman man1 = new Businessman("张三",fruit);
    10         
    11         System.out.println("man1:" + man1);
    12         //克隆
    13         Businessman man2 = (Businessman) man1.clone();
    14         
    15         System.out.println("man2 : " + man2);
    16         System.out.println("man1和man2前后是否为同一对象?" + (man1 == man2));
    17         System.out.println("两者的属性呢?"+(man1.getFruit()==man2.getFruit()));
    18     }
    19 }

    3、结果如下:

    man1:Businessman [name=张三, fruit=Apple [avgSize=5.0, price=90.0, productionArea=新疆]]
    man2 : Businessman [name=张三, fruit=Apple [avgSize=5.0, price=90.0, productionArea=新疆]]
    man1和man2前后是否为同一对象?false
    两者的属性呢?false
    

    使用序列化进行深克隆

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

        public  Object deepClone() 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 ois.readObject();
        }
    

      

  • 相关阅读:
    设计模式---行为变化模式之命令模式(Command)
    设计模式---数据结构模式之职责链模式(Chain of Responsibility)
    设计模式---数据结构模式之迭代器模式(Iterate)
    WinCE全屏手写输入法
    .net下所有DLL(API)查询,转换C#代码
    在线cron表达式生成器
    完全卸载vs2013、vs2015的方法
    java微信 客服接口-发消息 中文乱码
    【路由达人】简单两步搞定小米路由新增功能-DDNS(解析域名地址转向在线工具)
    微信公众平台开发入门教程
  • 原文地址:https://www.cnblogs.com/JiangWJ/p/10801449.html
Copyright © 2020-2023  润新知