原型模式
定义
原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。
通俗解释
比如有些人喜欢写文章,但是如果从头到尾原创的话太麻烦了,那么他可以上网去搜索,找一篇写得不错的文章,然后复制下来,做一些修改,最后发布就是自己的文章了。这其实就使用了原型模式的设计模式,创建一个对象过于麻烦的时候,我们只需要创建一次,后面再创建的话只需要对原对象进行克隆即可。
不使用原型模式的问题
假设我们有一个用户User的类,类里面有很多字段,当我们创建对象时,就会像这样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class Main { public static void main(String[] args) throws Exception { User user = new User(); user.setId(1); user.setName("张三"); user.setAge(18); user.setJob("程序员"); user.setSchool("家里蹲大学"); user.setNation("汉族"); user.setGender((byte)0); user.setPhone("110"); user.setPoliticalFeatures("群众"); user.setEducation("大学本科"); User user1 = new User(); user1.setId(2); user1.setName("李四"); user1.setAge(18); user1.setJob("程序员"); user1.setSchool("家里蹲大学"); user1.setNation("汉族"); user1.setGender((byte)0); user1.setPhone("111"); user1.setPoliticalFeatures("群众"); user1.setEducation("大学本科"); } }
|
不难看出上面的代码有以下问题:
1.user对象有10个字段,明显在创建第二个user对象的时候有很多重复的设值的操作。在实际项目中,肯定还不止设置10个字段,那么就会显得很难看。
2.创建对象如果消耗资源很多的话,这样多次去创建设值肯定会造成资源浪费。
对于以上的问题,我们可以使用原型模式进行优化。
使用Cloneable接口优化
java提供了一个Cloneable接口,可以实现克隆对象的用途,怎么实现,请看以下代码:
1 2 3 4 5 6 7 8 9 10 11
| public class User implements Cloneable { @Override public User clone() throws CloneNotSupportedException { return (User) super.clone(); } }
|
然后就可以把main()方法的代码改成以下这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public static void main(String[] args) throws Exception { User user = new User(); user.setId(1); user.setName("张三"); user.setAge(18); user.setJob("程序员"); user.setSchool("家里蹲大学"); user.setNation("汉族"); user.setGender((byte)0); user.setPhone("110"); user.setPoliticalFeatures("群众"); user.setEducation("大学本科"); User user1 = user.clone(); user1.setId(2); user1.setName("李四"); user1.setPhone("111"); System.out.println(user1); }
|
你是不是有疑问,这两个user对象内存地址是否一致呢?我们可以打印出来看看:
1 2
| com.yehongzhi.httpclient.model.User@4c873330 com.yehongzhi.httpclient.model.User@119d7047
|
内存地址是不一样的,所以我们可以得出一个结论:克隆出来的对象是一个新的对象。
问题:克隆方法的底层是不是调用了构造器创建了一个对象的呢?
我们可以在构造器上面加一些打印语句来验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class User implements Cloneable { public User() { System.out.println("调用了无参构造器"); } @Override public User clone() throws CloneNotSupportedException { System.out.println("调用了clone()方法"); return (User) super.clone(); } }
|
我们运行main()方法后,可以看到控制台打印信息如下:
只调用了一次构造器,我们可以得出结论:
clone()方法不是调用了构造器创建对象的。
如果你刨根究底,究竟clone()方法是怎么创建对象的,其实也很简单,打开源码:
1 2 3 4 5 6 7 8 9 10
| public class Object {
private static native void registerNatives(); static { registerNatives(); } protected native Object clone() throws CloneNotSupportedException; }
|
native修饰的方法是什么意思呢?意思就是这个方法的实现不是用java,而是C/C++实现。这个native关键字我们可以单独写一篇文章细讲,这里就不深入展开。底层的实现逻辑就是拷贝一份数据,开辟一块新的内存。所以拷贝出来的对象,打印的内存地址和原来的对象不一样。
使用Cloneable接口的问题
使用Cloneable接口是不是就完美的呢,其实并不是,因为如果一个对象的字段也是一个对象,是一个引用数据类型时,那就会有问题。请看以下代码:
我们增加一个对象IdCard类
1 2 3 4 5 6 7 8 9 10
| public class IdCard {
private String cardNo;
private Integer validityPeriod;
private Date createDate; }
|
1 2 3 4 5 6 7 8
| public class User implements Cloneable { private IdCard idCard; }
|
然后我们在main()方法赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class Main {
public static void main(String[] args) throws Exception { User user = new User(); IdCard idCard = new IdCard(); idCard.setCardNo("111111"); idCard.setCreateDate(new Date()); idCard.setValidityPeriod(10); user.setIdCard(idCard); User user1 = user.clone(); System.out.println(user.getIdCard()); System.out.println(user1.getIdCard()); user1.getIdCard().setCardNo("222222"); System.out.println(user.getIdCard().getCardNo()); } }
|
明显这样的克隆是有巨大的问题的,因为项目中不可能只有基本数据类型。那怎么解决呢?
1 2 3 4 5 6 7 8
| public class IdCard implements Cloneable{ @Override protected IdCard clone() throws CloneNotSupportedException { return (IdCard)super.clone(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class User implements Cloneable { @Override public User clone() throws CloneNotSupportedException { User user = (User) super.clone(); IdCard idCard = user.getIdCard(); user.setIdCard(idCard.clone()); return user; } }
|
最后我们再调用main()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Main { public static void main(String[] args) throws Exception { User user = new User(); IdCard idCard = new IdCard(); idCard.setCardNo("111111"); user.setIdCard(idCard); User user1 = user.clone(); System.out.println(user.getIdCard()); System.out.println(user1.getIdCard()); user1.getIdCard().setCardNo("222222"); System.out.println(user.getIdCard().getCardNo()); } }
|
使用序列化实现深克隆
上面使用Cloneable接口的方式,被称为浅克隆,如果你想要克隆的源对象里面又有对象时,里面的对象也要实现Cloneable接口,然后修改源对象的clone()方法,这样就非常麻烦,而且当扩展时会破坏开闭原则。
解决方法,我们可以采用序列化对象的方式,实现深克隆呢?请看以下代码:
User对象实现Serializable接口:
1 2 3
| public class User implements Serializable { private static final long serialVersionUID = 8656071024384993135L; }
|
IdCard对象实现Serializable接口:
1 2 3
| public class IdCard implements Serializable { private static final long serialVersionUID = -422430076410272813L; }
|
创建一个工具类CloneUtil实现深克隆:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class CloneUtil { @SuppressWarnings("unchecked") public static <T> T depthClone(T t, Class<T> clazz) throws Exception { ByteArrayOutputStream baos = null; ObjectOutputStream ous = null; ByteArrayInputStream bais = null; ObjectInputStream ois = null; try { baos = new ByteArrayOutputStream(); ous = new ObjectOutputStream(baos); ous.writeObject(t); bais = new ByteArrayInputStream(baos.toByteArray()); ois = new ObjectInputStream(bais); return (T) ois.readObject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("深克隆出现异常"); } finally { if (ous != null) {ous.close();} if (baos != null) {baos.close();} if (bais != null) {bais.close();} if (ois != null) {ois.close();} } } }
|
验证是否深克隆,在main()方法中打印内存地址查看即可:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Main {
public static void main(String[] args) throws Exception { User user = new User(); < |