原型模式是设计模式中算是简单的一种设计模式了,因为它有语言实现的支撑,只需要调用定义好了的方法即可使用,在写这篇设计模式之前,我看过很多资料,有几点比较疑惑的点,在这篇文章中验证,也顺便描述一下什么是原型模式。
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
这个定义也是很简单了,主要意思就是用一个实例当作是一个原型,通过拷贝这个实例去创建新的对象,就像西游记的美猴王使用猴毛去分身,可以很简单的创建各种美猴王对象,而不需要去初始化各种美猴王属性,比如身高体重之类。
先上一个简单的原型模式实例,再抛出问题。
/**
* @description:
* @author: linyh
* @create: 2018-11-05 16:27
**/
public class Weapon {
private int size;
public Weapon(int size) {
this.size = size;
}
public int getSize() {
return size;
}
public void changeSize(){
size++;
}
}
/**
* @description:
* @author: linyh
* @create: 2018-11-05 16:27
**/
public class Monkey implements Cloneable{
private int age;
private String name;
private Weapon weapon;
public Monkey() {
this.age = 18;
this.name = "猴子";
this.weapon = new Weapon(10);
}
public void changeAge(){
age ++;
}
public void changeName(){
name = name + "changed ";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public Weapon getWeapon() {
return weapon;
}
}
monkey中的change方法后面实验会用到。下面上测试类。
public class test {
public static void main(String[] args) throws CloneNotSupportedException {
Monkey monkey = new Monkey();
Monkey copyMonkey = (Monkey)monkey.clone();
System.out.println("两个对象是否一样: " + (monkey == copyMonkey));
System.out.println("正版猴子的各个属性: " + monkey.getName() +"," + monkey.getAge());
System.out.println("复制猴子的各个属性: " + copyMonkey.getName() +"," + copyMonkey.getAge());
System.out.println("两个猴子的武器是否一样: " + (monkey.getWeapon() == copyMonkey.getWeapon()));
}
}
控制台打印
这里发现,克隆出来的对象是不一样的,如预期所料,属性都有初始化好,然后对象不同。但是这里的武器却还是同一个武器!这会引发什么问题呢?上测试代码
System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
monkey.getWeapon().changeSize();
System.out.println("正版猴子武器SIZE改变成了" +monkey.getWeapon().getSize());
System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
控制台打印
可以看到,因为是同一个对象,我正版猴子把武器的size变化了,复制的猴子什么都没干武器也会变化,这确实不符合逻辑。
以上复制称为浅复制,意思是如果有引用类型,会把引用的地址直接复制过来,但如果是8种基本类型就没事,下面验证一下。
System.out.println("正版的猴子的年龄: " + monkey.getAge());
System.out.println("复制的猴子的年龄: " + copyMonkey.getAge());
monkey.changeAge();
System.out.println("正版猴子年龄改变成了" + monkey.getAge());
System.out.println("复制的猴子的年龄: " + copyMonkey.getAge());
控制台打印
虽然我这里改变了正版猴子的年龄,但复制的猴子年龄依旧的18。
抛出第一个问题:哪些类型需要深拷贝?
我曾经在一个知名博主的一篇博客上看到一个论点,表示很疑惑。
因为我不记得在哪篇文章上有看到说包括9种类型(8种基本类型加上String类型),都不需要深拷贝 ,验证一下就知道了。
System.out.println("正版的猴子的姓名: " + monkey.getName());
System.out.println("复制的猴子的姓名: " + copyMonkey.getName());
monkey.changeName();
System.out.println("正版猴子姓名变成了" + monkey.getName());
System.out.println("复制的猴子的姓名: " + copyMonkey.getName());
控制台打印
查了一下资料,发现String 不改变值是浅copy。但给name赋值时(set方法等),常量值的地址是固定不变的,字符串对象只能改变自己的引用了,原来对象的引用不会变,效果上这是相当于实现了深copy。
所以这里真正的结论是:8种基本类型加上String类型,都可以不需要深拷贝,底层自动会进行深拷贝。
抛出第二个问题:实现深拷贝的方法?
据我所知有两种,分别测试一下把。
第一种方法:将需要深拷贝的引用对象再次调用clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Monkey monkey =(Monkey) super.clone();
monkey.weapon = (Weapon) this.weapon.clone();
return monkey;
}
这里将Monkey的clone方法改造了一下,所以Weapon中也需要加入clone方法。
/**
* @description:
* @author: linyh
* @create: 2018-11-05 16:27
**/
public class Weapon implements Cloneable{
private int size;
public Weapon(int size) {
this.size = size;
}
public int getSize() {
return size;
}
public void changeSize(){
size++;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试一下
这里的结果就符合了我们预期的效果了。
第二种方法:序列化再反序列化
改变一下Monkey的clone方法,此方法需要将需要深拷贝的类实现Serializable接口(Monkey、Weapon)
@Override
protected Object clone() throws CloneNotSupportedException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
//序列化
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
oos.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
这里给Monkey多加一个属性以及get和change方法便于测试。
private List<Integer> list;
public void changeList(){ list.add(2); }
测试类
Monkey monkey = new Monkey();
Monkey copyMonkey = (Monkey)monkey.clone();
System.out.println("两个对象是否一样: " + (monkey == copyMonkey));
System.out.println("正版猴子的各个属性: " + monkey.getName() +"," + monkey.getAge());
System.out.println("复制猴子的各个属性: " + copyMonkey.getName() +"," + copyMonkey.getAge());
System.out.println("两个猴子的武器是否一样: " + (monkey.getWeapon() == copyMonkey.getWeapon()));
System.out.println("两个猴子的List是否一样: " + (monkey.getList() == copyMonkey.getList()));
System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
monkey.getWeapon().changeSize();
System.out.println("正版猴子武器SIZE改变成了" +monkey.getWeapon().getSize());
System.out.println("复制的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
System.out.println("正版的猴子的ListSize: " + monkey.getList().size());
System.out.println("复制的猴子的ListSize: " + copyMonkey.getList().size());
monkey.changeList();
System.out.println("正版猴子ListSize改变成了" +monkey.getList().size());
System.out.println("复制的猴子的ListSize: " + copyMonkey.getList().size());
控制台打印
这里就完成了深拷贝过程,对比两种方法,后者,只需要序列化与反序列化以及实现序列化接口即可,如果是第一种方法,需要将所有的需要深拷贝的引用都调用其上的clone方法,对比而言,像上面一个例子既有list还有weapon这种多个需要深拷贝引用的,个人觉得还是使用第二种序列化的方式比较便捷,如果只有一个引用像第一种的例子只有一个weapon需要深拷贝,就可以考虑weapon也实现一个clone,然后在monkey调用clone的时候也把weapon clone一下,毕竟只有一个。
Clone(原型模式)的优点
- clone方法底层原理是JVM直接复制内存块的操作,所以在速度上比直接new来的快。
- 初始化过程比较复杂,类中属性复杂且需要复制时,使用原型模式更快捷,不需要一个个设置属性。
Clone(原型模式)的缺点
从上面的例子也可以看出来,需要去实现一个Cloneable接口以及其clone方法,如果需要克隆的类中有需要深拷贝类型的还需要在clone额外下功夫,比较繁琐,代码量需要增加不少。现实中我们可以在一个类定义好clone方法,需要克隆的类都可以去继承这个类,也是一个节省代码量的做法。
由于这里的原型模式clone方法并不是平时new对象的操作,所以在克隆的时候不会再执行构造器的方法,而是直接复制内存块,所以要注意的是构造器的方法克隆时不会执行,这里不做测试,感兴趣可以自己在构造器中加入输出语句方法试一下。