一、简介
原型模式(Prototype)
指的是用原型实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象
什么时候可以使用原型模式
- 当一个系统应该独立于它的产品创建、构成和表达时
- 当要实例化的类是在运行时制定时,例如动态装载
- 为了避免创建一个与产品类层次平行的工厂类层次时
- 当一个类的实例只能有几个不同状态组合中的一种时。
二、UML类图
解释一下图中各个角色
- Prototype:声明一个克隆自身的接口
- ConcretePrototype:实现一个克隆自身的操作
- Client:让一个原型克隆自身从而创建一个新的对象
三、Java与原型模式
在Java中,其实就有语言本身为我们提供好的原型模式,我们只需要写实现就可以了。
我们都知道Java中Object是所有对象的夫类,而Object就为我们提供了一个clone()
方法,我们只需要让我们的类实现clone()
就可以了。
其实仔细想一下,Object
不就相当于图中的Prototype
,而我们自己要支持克隆的类不就相当于ConcretePrototype
吗,框架搭好了,我们把最重要的方法实现写好就好了鸭~
⚠️注意:唯一需要注意的一点就是我们需要让想要克隆本身的类继承Cloneable
接口,表示其支持克隆。
代码如图所示:
/**
* @author ZhongJing </p>
* @Description </p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person implements Cloneable {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private String age;
/**
* 性别
*/
private String gender;
/**
* 宠物
*/
private Pets pets;
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = null;
person = (Person) super.clone();
return person;
}
}
这样我们就写好了一个Person
的克隆方法,来试一下效果如何
/**
* @author ZhongJing </p>
* @Description </p>
*/
public class Client {
public static void main(String[] args) {
Person person = new Person();
person.setName("张三");
person.setAge(18);
person.setGender("男");
Pets pets = new Pets();
pets.setName("旺财");
pets.setAge(5);
person.setPets(pets);
System.out.println("原型:" + person);
try {
Person personClone = (Person) person.clone();
System.out.println("克隆:" + personClone);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果如下:
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
可以看出是克隆成功的,但是调用Object的clone()
方法其实是有一些问题的,我们如果不重写Object的clone()
方法的话,它其实是浅拷贝的,浅拷贝的话,级联对象会直接按引用传递给新的克隆对象,简而言之就是原型对象的中的级联对象和浅拷贝克隆出的对象其实是同一个对象,当我们操作其中一个对象的时候另一个对象也会随之改变,这种问题在我们有一些特殊需求的情况下是不符合我们预期的,如下面代码运行结果:
/**
* @author ZhongJing </p>
* @Description </p>
*/
public class Client {
public static void main(String[] args) {
Person person = new Person();
person.setName("张三");
person.setAge(18);
person.setGender("男");
Pets pets = new Pets();
pets.setName("旺财");
pets.setAge(5);
person.setPets(pets);
System.out.println("原型:" + person);
try {
Person personClone = (Person) person.clone();
System.out.println("克隆:" + personClone);
// 改变级联对象的属性
pets.setName("如花");
System.out.println("原型:" + person);
System.out.println("克隆:" + personClone);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=如花, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=如花, age=5))
四、浅拷贝和深拷贝
浅拷贝:
在浅拷贝中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅拷贝中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制
深拷贝:
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制
Java中如何实现浅拷贝?
浅拷贝我们在第三章的时候就已经实现过了,并且也阐明了浅拷贝会发生的一些弊端(算是弊端吧)这里就不过多赘述
Java中如何实现深拷贝?
Java中实现深拷贝有两种方式
- 重写
clone()
方法,不仅仅只调用super.clone();
而是自己制定克隆方式 - 使对象实现序列化接口(Serializable),通过将对象输入流再输出来拷贝对象
五、Java深拷贝的方式
5.1重写clone()
重写clone的方式其实也是很简单的,和浅拷贝类似,只不过我们需要额外的对级联的对象进行一次单独的克隆,然后在set到克隆对象里就好了,但是需要我们对级联的对象也实现clone()
方法,如果有级联多层(级联对象还有级联对象)要对每一级的级联对象都实现clone()
方法,知道某个级联对象没有依赖的对象而全是基础类型为止。
直接上代码
/**
* @author ZhongJing </p>
* @Description </p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person implements Cloneable {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private String gender;
/**
* 宠物
*/
private Pets pets;
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = null;
// 先克隆本对象
person = (Person) super.clone();
// 对级联的对象进行额外的处理
Pets petsClone = (Pets) person.getPets().clone();
// 将级联对象的克隆set到本对象中
person.setPets(petsClone);
return person;
}
}
/**
* @author ZhongJing </p>
* @Description </p>
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Pets implements Cloneable {
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private Integer age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
如上述代码所示,我们来实验一下,改变原型的级联属性克隆对象的级联属性是否会发生变化
/**
* @author ZhongJing </p>
* @Description </p>
*/
public class Client {
public static void main(String[] args) {
Person person = new Person();
person.setName("张三");
person.setAge(18);
person.setGender("男");
Pets pets = new Pets();
pets.setName("旺财");
pets.setAge(5);
person.setPets(pets);
System.out.println("原型:" + person);
try {
Person personClone = (Person) person.clone();
System.out.println("克隆:" + personClone);
// 改变级联对象的属性
pets.setName("如花");
System.out.println("原型:" + person);
System.out.println("克隆:" + personClone);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=如花, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
通过结果可以看得出,这次我们改变原型对象的级联属性后,克隆对象并没有随之改变,也可以证明我们的深拷贝是成功了的。
5.2序列化深拷贝
我们也可以通过序列化然后将对象写入流再写出流的方式来对对象进行克隆,并且不需要把每一个级联对象的clone()
方法都实现而且原型对象也不需要实现Cloneable
接口,而是需要实现Serializable
接口,使得对象支持序列化,这种方式的好处就是只需要在原型和级联的对象中实现Serializable
接口,不需要其余的多余操作,具体克隆细节只在原型中定义。
直接看代码:
/**
* @author ZhongJing </p>
* @Description </p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person implements Serializable {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private String gender;
/**
* 宠物
*/
private Pets pets;
/**
* 自定义克隆方法(通过序列化实现深拷贝)
*
* @return 返回克隆的对象
*/
public Object clonePerson() {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
// 创建输出流
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// 将本类对象写入流
oos.writeObject(this);
// 创建输出流
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
// 将对象从流中写出
Person personClone = (Person) ois.readObject();
// 返回对象
return personClone;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* @author ZhongJing </p>
* @Description </p>
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Pets implements Serializable {
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private Integer age;
}
来实验一下:
/**
* @author ZhongJing </p>
* @Description </p>
*/
public class Client {
public static void main(String[] args) {
Person person = new Person();
person.setName("张三");
person.setAge(18);
person.setGender("男");
Pets pets = new Pets();
pets.setName("旺财");
pets.setAge(5);
person.setPets(pets);
System.out.println("原型:" + person);
Person personClone = (Person) person.clonePerson();
System.out.println("克隆:" + personClone);
pets.setName("如花");
System.out.println("原型:" + person);
System.out.println("克隆:" + personClone);
}
}
运行结果如下:
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
原型:Person(name=张三, age=18, gender=男, pets=Pets(name=如花, age=5))
克隆:Person(name=张三, age=18, gender=男, pets=Pets(name=旺财, age=5))
根据结果可以看到,我们使用序列化实现深拷贝的方式同样也实现了
六、总结
因为Java对原型模式的支持,所以原型模式在Java是很容易实现的一件事情。
这篇博客主要的想法也就是区分一下浅拷贝和深拷贝的区别,以及Java实现深拷贝的两种方式,其实并不算单纯的讲设计模式了(个人认为)。嗯……那就先这样吧,Over!