原型模式(实现clone)
场景:克隆羊
创建10只姓名为Tom,年龄为1,颜色为白色的羊
原始方法(灵活性差)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep{
private String name;
private Integer age;
private String color;
}
public class CloneSheepDemo {
public static void main(String[] args) {
Sheep prototypeSheep=new Sheep("tom",1,"白");
Sheep sheep1=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep2=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep3=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep4=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep5=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
}
}
先创建一只满足要求的羊作为原型,然后调用有参构造方法new十只羊,参数来源为这只羊
缺点:不够灵活和优雅,需要cv大量的代码,假如后面这只羊需要添加一个属性,所以创建这个羊的代码都需要改,非常麻烦
解决方法:原型模式(简单而优雅)
让需要clone的类实现Cloneable接口,然后实现里面的Clone方法
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/**
* @author 李天航
*/
public class CloneSheepDemo {
@SneakyThrows
public static void main(String[] args) {
Sheep prototypeSheep=new Sheep("tom",1,"白");
Sheep sheep1= (Sheep) prototypeSheep.clone();
Sheep sheep2= (Sheep) prototypeSheep.clone();
Sheep sheep3= (Sheep) prototypeSheep.clone();
Sheep sheep4= (Sheep) prototypeSheep.clone();
Sheep sheep5= (Sheep) prototypeSheep.clone();
System.out.println(sheep1);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
System.out.println(sheep5);
}
}
这样在进行clone的适合我们只需要调用原型的clone方法即可,如果后面增加了属性我们也不需要修改代码,由父类Object来帮我们完成拷贝工作,我们也可以加上一些自己的逻辑。同时clone方法是native方法,由字节码来高效完成拷贝工作,比我们调用构造函数的效率要高。同时客户端clone时无需关系clone具体是怎么实现的,具体的实现交给clone方法内部来完成。
而实现Cloneable接口,为了让这个类可克隆,否则会抛出CloneNotSupportedException异常
Spring源码
我们在注入bean的时候,除了可以将bean设置为单例的(singleton),也可以将bean设置为多例的(prototype),设置为多例的时候,创建bean的方式就是原型模式
浅拷贝和深拷贝
浅拷贝
假如类中有一个引用类型的变量:
@NoArgsConstructor
@AllArgsConstructor
class Hobby{
String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
此时如果直接使用Object的拷贝方法,基本数据类型和String类型可以被正常复制,而引用类型则是进行引用复制而不会创建新的对象
可以看到所以的Sheep对象中的hobby都指向同一个对象
此时的拷贝就是浅拷贝,相当于对上述所以字段使用等号=
来赋值
深拷贝
深拷贝就是字段中依赖的其他对象也都是新的对象,而不是和原型指向相同的对象
可以通过将依赖的对象拿出来单独进行clone,这样就能达到深拷贝:
@NoArgsConstructor
@AllArgsConstructor
class Hobby implements Cloneable{
String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
@Override
protected Object clone() throws CloneNotSupportedException {
Sheep clone = (Sheep) super.clone();
clone.setHobby((Hobby) hobby.clone());
return clone;
}
}
但是如果依赖的对象很多,甚至依赖的对象还依赖了其他类的对象,这样一个一个clone会很麻烦,因而我们可以采用JDK的序列化机制来实现深拷贝。
@NoArgsConstructor
@AllArgsConstructor
class Hobby implements Serializable{
String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Serializable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
protected Object deepClone() throws CloneNotSupportedException {
ByteArrayOutputStream byteArrayOutputStream=null;
ObjectOutputStream objectOutputStream=null;
ByteArrayInputStream byteArrayInputStream=null;
ObjectInputStream objectInputStream=null;
try {
byteArrayOutputStream=new ByteArrayOutputStream();
objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
byteArrayInputStream=new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
objectInputStream=new ObjectInputStream(byteArrayInputStream);
return objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
assert objectOutputStream != null;
assert objectInputStream != null;
objectOutputStream.close();
objectInputStream.close();
byteArrayInputStream.close();
byteArrayOutputStream.close();
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
}
return null;
}
}
看着代码很多,其实大部分都是在处理异常和关闭流和初始化流,核心代码就六行:
//开启一个字节数组流作为缓冲区
byteArrayOutputStream=new ByteArrayOutputStream();
//将这个对象序列化并写入上面的缓冲区
objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
//创建对象数据流并从缓冲区中读进来进行反序列化
byteArrayInputStream=new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
objectInputStream=new ObjectInputStream(byteArrayInputStream);
//读对象的时候会创建一个新的对比保存反序列化的结果
return objectInputStream.readObject();
因为需要序列化和反序列化,所以需要实现Serializable,其实就只是一个标志,表示这个对象可以序列化
另外,因为我们的深拷贝没有用到super.clone()所以可以不用实现Cloneable接口,自己另写一个方法即可
如图所示,所有的Hobby对象都不一样
使用JDK的序列化机制就不需要我们一个一个手动clone引用类型的对象了,十分方便,也推荐使用
特点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvzpd2Ub-1653708381573)(https://s2.loli.net/2022/05/28/f4z62swvNlWPuIn.png)]
原型模式深拷贝违背了OCP原则(是吧,想要遵循所有的原则几乎是不可能的),因为我们要为之前写好的类配备一个clone方法,或者让他实现一个Serializable接口。不过这个修改成本其实是很低的,修改后不会改变原有的业务逻辑,并且我们一般会在开发时直接继承Serializable接口,也就消除了这个问题。(我们只需要知道它违反了这个原则即可,需要用的时候还是正常用,不用原型模式耦合度会更大)