1、场景推导
假设一个场景,我们现在要写周报
发现周报里面需要填很多信息,例如姓名、部门、职位等等
这些信息基本都不会变,但是每次填周报我们又需要填一遍
这样显然不合理,我们其实想的是,写周报只需要填汇报内容
即可
那应该怎么办呢?是不是希望能够保存一下上一次编辑的模板,下次直接修改内容
就可以了
请看下面栗子
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport{
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private LocalDateTime submitDate;
}
public class App {
public static void main(String[] args) {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("King")
.setSummary("本周主要完成了七大设计原则")
.setPlain("在下周的刷完面试题")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
// 简单输出一下
System.out.println(weekReport);
// 第二周周报
WeekReport weekReport2 = new WeekReport();
weekReport2.setEmp("King")
.setSummary("本周主要完成了设计模式的学习")
.setPlain("在下周会搞懂30道算法")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
System.out.println(weekReport2);
}
}
我们会发现其实下一周要进行汇报时,我们只需要修改总结和下周计划
的内容
但是现在我们却只能再新建一个对象重复上述代码
我们想要的是直接克隆
出一个对象来,将需要改变的填一下,不变的用之前的就好
通过克隆
来实现上面的需求,在Java中只需要实现Cloneable
,并重写克隆方法即可
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport implements Cloneable {
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private LocalDateTime submitDate;
//重写方法后将修饰符改为public
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("King")
.setSummary("本周主要完成了七大设计原则")
.setPlain("在下周的刷完面试题")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
// 简单输出一下
System.out.println(weekReport);
// 第二周周报
WeekReport weekReport2 = (WeekReport) weekReport.clone();
weekReport2.setSummary("本周主要完成了设计模式的学习")
.setPlain("在下周会搞懂30道算法")
.setSubmitDate(LocalDateTime.now());
System.out.println(weekReport2);
}
}
这就是原型设计模式,这里还有几个点要注意一下
-
上面的克隆其实只是
浅拷贝
-
使用clone方法创建对象,并不会调用该对象的构造器
因为Object类的clone方法是一个本地方法,它直接操作内存中的
二进制流
,特别是复制大对象时,性能的差别非常明显。
2、浅拷贝深拷贝
类型 | 特点 |
---|---|
浅拷贝 | 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。 |
深拷贝 | 被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。 |
说人话
-
浅拷贝:
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值。
- 如果属性是引用类型,拷贝的就是指向内存的地址 ,所以如果其中一个对象改变了这个引用类型的值,就会影响到另一个对象。
- 浅拷贝使用默认的clone()方法来实现。
-
深拷贝:
-
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。
-
且修改新对象不会影响原对象。
-
看下面的栗子:可以把setSubmitDate
看做一个指针,弄了一个新对象给它,会在堆空间里面新申请一块内存,结果自然就不一样了
而weekReport2改变了引用类型的值,就会影响到weekReport对象
那我们如何深拷贝呢?其实也很简单,既然原来的Object.clone()
方法无法克隆引用类型,那我们自己来克隆就好了
其中大部分类其实都是实现了clone方法的
@Override
protected Object clone() throws CloneNotSupportedException {
WeekReport cloneWeekReport = (WeekReport) super.clone();
// Date类型需要自己手动进行克隆
Date cloneSubmitDate = (Date) cloneWeekReport.getSubmitDate().clone();
cloneWeekReport.setSubmitDate(cloneSubmitDate);
return cloneWeekReport;
}
但是这样很麻烦,如果我们克隆的是自己定义的对象,或者是对象里面套对象,这样层层嵌套的形式,就会非常麻烦
3、封装深拷贝工具
所以我们一般是用序列化和反序列化来做的,并且会封装成一个工具,这个拷贝在开发中还是非常常见的,例如我们经常将一个PO转换为一个VO(就是拷贝对象),一般用Spring自带的BeanUtils
,但是它只能浅拷贝,我们现在自己封装一个,可以选择继承Spring自带的BeanUtils
,或者就放deepClone
这一个方法就行
/**
* 拷贝Bean
*/
@Slf4j
public final class BeanUtil extends BeanUtils {
private BeanUtil() {
throw new UnsupportedOperationException();
}
/**
* 拷贝属性
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) {
if (checkNull(source, targetClass)) {
return null;
}
try {
T newInstance = targetClass.newInstance();
copyProperties(source, newInstance);
return newInstance;
} catch (Exception e) {
log.error("error: ", e);
return null;
}
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T object) {
T cloneObject = null;
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
cloneObject = (T) objectInputStream.readObject();
objectInputStream.close();
} catch (ClassNotFoundException | IOException e) {
log.info("拷贝异常::", e);
}
return cloneObject;
}
public static <T> T copyProperties(Object source, Class<T> targetClass, String... ignoreProperties) {
if (checkNull(source, targetClass)) {
return null;
}
try {
T newInstance = targetClass.newInstance();
copyProperties(source, newInstance, ignoreProperties);
return newInstance;
} catch (Exception e) {
log.error("error: ", e);
return null;
}
}
/**
* 拷贝集合
*/
public static <T> List<T> copyProperties(List<?> sources, Class<T> targetClass) {
if (checkNull(sources, targetClass) || sources.isEmpty()) {
return new ArrayList<>();
}
return sources.stream().map(source -> copyProperties(source, targetClass)).collect(Collectors.toList());
}
private static <T> boolean checkNull(Object source, Class<T> targetClass) {
return Objects.isNull(source) || Objects.isNull(targetClass);
}
}
现在在来看看原来的拷贝,这里需要注意进行序列化的对象需要实现Serializable
接口
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport implements Serializable {
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private Date submitDate;
}
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("King")
.setSummary("本周主要完成了七大设计原则")
.setPlain("在下周的刷完面试题")
.setDepartment("互联网事业部")
.setSubmitDate(new Date());
// 简单输出一下
System.out.println("weekReport的时间为:"+ weekReport.getSubmitDate());
// 第二周周报 深拷贝
WeekReport weekReport2 = BeanUtil.deepClone(weekReport);
weekReport2.setSummary("本周主要完成了设计模式的学习")
.setPlain("在下周会搞懂30道算法");
// 这样才是修改同一个引用指向堆空间里面的值
weekReport2.getSubmitDate().setTime(0);
System.out.println(weekReport == weekReport2);
System.out.println("weekReport2的时间为:"+ weekReport2.getSubmitDate());
System.out.println("weekReport的时间为:"+ weekReport.getSubmitDate());
}
}
可以看到修改新对象不会影响到原来的对象。