zoukankan      html  css  js  c++  java
  • 设计模式(16)>原型模式

    原型模式

    一、原型模式定义

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

    Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节。

    工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

    二、模式解读

    原型模式包含如下角色:

    • 抽象原型(abstract prototype)java中所有类都继承自Object,且Object实现了clone()方法,所以所有的对象都可以通过实现clonable接口实现具体原型;
    • 具体原型类(concrete prototype):具体原型类如果实现了clone()方法,需要实现clonable接口,否则会抛出CloneNotSupported异常;
    • 客户类(client):用来使用模式

    二、Java对Prototype支持

    因为Java中的提供clone()方法来实现对象的克隆,所以Prototype模式实现一下子变得很简单.在Java中Prototype 模式变成clone()方法的使用,由于Java 的纯洁的面向对象特性,使得在Java 中使用设计模式变得很自然,两者已经几乎是浑然一体了。这反映在很多模式上,如Interator 遍历模式。对象拷贝时,类的构造函数是不会被执行的,这个从原理来讲也是可以讲得通的,Object类的clone方法的原理是从内存中(具体的说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是非常正常的了。

    Person.java

    class Person implements Cloneable {
    	// 姓名
    	private String name;
    	// 父亲
    	private Person father;
    
    	public Person(String _name) {
    		name = _name;
    	}
    
    	public Person(String _name, Person _parent) {
    		name = _name;
    		father = _parent;
    	}
    
    	
    	
    	// 拷贝的实现
    	@Override
    	public Person clone() {
    		Person p = null;
    		try {
    			p = (Person) super.clone();
    		} catch (CloneNotSupportedException e) {
    			e.printStackTrace();
    		}
    		return p;
    	}
    
    	
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Person getFather() {
    		return father;
    	}
    
    	public void setFather(Person father) {
    		this.father = father;
    	}
    }

    Client 

    public class Client {
    	public static void main(String[] args) {
    		// 定义父亲
    		Person f = new Person("张三");
    		// 定义大儿子
    		Person firstChild = new Person("张三大儿子", f);
    		// 小儿子的信息是通过大儿子拷贝过来的
    		Person secondChild = firstChild.clone();
    	}
    }

    三、浅拷贝与深拷贝  

    浅拷贝存在的问题,让小儿子认个干爹

    public class Client {
    	public static void main(String[] args) {
    		// 定义父亲
    		Person f = new Person("张三");
    		// 定义大儿子
    		Person firstChild = new Person("张三大儿子", f);
    		// 小儿子的信息是通过大儿子拷贝过来的
    		Person secondChild = firstChild.clone();
    		secondChild.setName("张三小儿子");
    		
    		// 小儿子认干爹
    		secondChild.getFather().setName("李四");
    		
    		System.out.println(firstChild.getName() + " 的父亲是 " + firstChild.getFather().getName());
    		System.out.println(secondChild.getName() + " 的父亲是 " + secondChild.getFather().getName());
    	}
    }

    运行结果

    张三大儿子的父亲是李四

    张三小儿子的父亲是李四

    大儿子的父亲也成了“李四”? 两个儿子都没有了,岂不是要气死“张三”了!

    出现这个问题的原因就在于clone()方法,我们知道所有类都继承自Object,Object 提供了一个对象拷贝的默认方法,即上面代码中的super.clone 方法,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝幸运的是java提供的大部分的容器类都实现了Cloneable接口。所以实现深拷贝并不是特别困难。它的拷贝规则具体如下:

    (1)基本类型

    如果变量是基本类型,则拷贝其值,比如int、float 等。

    (2)对象

    如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java 中是很疯狂的,因为它突破了访问权限的定义:一个private 修饰的变量,竟然可以被两个不同的实例对象访问,这让Java的访问权限体系情何以堪!

    (3)String 字符串

    这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String 是一个基本类型。

    浅复制

    复制了值类型对象,对于引用类型对象,只复制了引用,它指向原来引用的对象。Java中clone为浅复制。
    深复制

    对值类型和引用类型的对象都生成一份新的拷贝. Java中可以通过串行化来进行深复制,前提是对象以及对象内部所引用的对象都是可串行化的,否则需要考虑将那些不可串行化的对象可否设为transient,排除在复制过程之外。

    深拷贝的实现

    class Person implements Cloneable {
    	// 姓名
    	private String name;
    	// 父亲
    	private Person father;
    
    	public Person(String _name) {
    		name = _name;
    	}
    
    	public Person(String _name, Person _parent) {
    		name = _name;
    		father = _parent;
    	}
    
    	// 拷贝的实现
    	@Override
    	public Person clone() {
    
    		Person p = null;
    
    		try {
    
    			p = (Person) super.clone();
    
    			p.setFather(new Person(p.getFather().getName()));
    
    		} catch (CloneNotSupportedException e) {
    
    			e.printStackTrace();
    
    		}
    
    		return p;
    
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Person getFather() {
    		return father;
    	}
    
    	public void setFather(Person father) {
    		this.father = father;
    	}
    }

    然后再运行,大儿子的父亲就不会是“李四”了。如此就实现了对象的深拷贝(DeepClone),保证拷贝出来的对象自成一体,不受“母体”的影响,和new 生成的对象没有任何区别。

    四、用序列化技术(Serilization)来完成对象的复制

    实现Cloneable 接口就具备了拷贝能力,那我们来思考这样一个问题:如果一个项目中有大量的对象是通过拷贝生成的,那我们该如何处理?每个类都写一个clone 方法,并且还要深拷贝?想想看这是何等巨大的工作量呀,是否有更好的方法呢?

    其实,可以通过序列化方式来处理,在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象,代码如下:

    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class CloneUtils {
    	// 拷贝一个对象
    	@SuppressWarnings("unchecked")
    	public static <T extends Serializable> T clone(T obj) {
    		// 拷贝产生的对象
    		T clonedObj = null;
    		try {
    			// 读取对象字节数据
    			ByteArrayOutputStream baos = new ByteArrayOutputStream();
    			ObjectOutputStream oos = new ObjectOutputStream(baos);
    			oos.writeObject(obj);
    			oos.close();
    			// 分配内存空间,写入原始对象,生成新对象
    			ByteArrayInputStream bais = new ByteArrayInputStream(baos
    					.toByteArray());
    			ObjectInputStream ois = new ObjectInputStream(bais);
    			// 返回新对象,并做类型转换
    			clonedObj = (T) ois.readObject();
    			ois.close();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    		return clonedObj;
    	}
    }

    此工具类要求被拷贝的对象必须实现Serializable 接口,否则是没办法拷贝的(当然,使用反射那是另外一种技巧),上一个建议中的例子只要稍微修改一下即可实现深拷贝,代码如下:

    class Person implements Serializable{
    
    private static final longserialVersionUID = 1611293231L;
    
    /* 删除掉clone 方法,其他代码保持不变*/
    
    }
    

    被拷贝的类只要实现Serializable 这个标志性接口即可,不需要任何实现, 当然serialVersionUID 常量还是要加上去的,然后我们就可以通过CloneUtils 工具进行对象的深拷贝了。用此方法进行对象拷贝时需要注意两点:

    (1)对象的内部属性都是可序列化的

    如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点来考虑,也需要把CloneUtils工具的异常进行细化处理。

    (2)注意方法和属性的特殊修饰符

    final关键字和clone有冲突

    import java.util.ArrayList;
    
    public class Thing implements Cloneable {
    	// 定义一个私有变量
    	private final ArrayList<String> arrayList = new ArrayList<String>();
    
    	@Override
    	public Thing clone() {
    		Thing thing = null;
    		try {
    			thing = (Thing) super.clone();
    			this.arrayList = (ArrayList<String>) this.arrayList.clone();
    		} catch (CloneNotSupportedException e) {
    			e.printStackTrace();
    		}
    		return thing;
    	}
    }

    报错:the final field ArrayList can not be assigned

    static变量的序列化问题会被引入到对象拷贝中来,这点需要特别注意,

    transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。

    五. 模式总结

    4.1 优点

    1. 隐藏了对象的创建细节,对有些初始化需要占用很多资源的类来说,对性能也有很大提高。
    2. 在需要新对象时,可以使用Clone来快速创建创建一个,而不用使用new来构建。

    4.2 缺点

    每一个类都需要一个Clone方法,而且必须通盘考虑。对于深拷贝来说,每个关联到的类型都不许实现Cloneable接口,并且每增加或修改一个字段是都需要更新Clone方法。

    4.3 适用场景

    1. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
    2. 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
    3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。

    参考

    编写高质量代码:改善Java程序的151个建议 (43 44)

  • 相关阅读:
    ASE19团队项目 beta阶段 model组 scrum report list
    ASE19团队项目 beta阶段 model组 scrum7 记录
    ASE19团队项目 beta阶段 model组 scrum6 记录
    ASE19团队项目 beta阶段 model组 scrum5 记录
    ASE19团队项目 beta阶段 model组 scrum4 记录
    ASE19团队项目 beta阶段 model组 scrum3 记录
    ASE19团队项目 beta阶段 model组 scrum2 记录
    ASE19团队项目 beta阶段 model组 scrum1 记录
    【ASE模型组】Hint::neural 模型与case study
    【ASE高级软件工程】第二次结对作业
  • 原文地址:https://www.cnblogs.com/xqzt/p/5637052.html
Copyright © 2011-2022 走看看