大家都知道,Java中的克隆有深克隆和浅克隆,今天我们谈谈深克隆的几种实现方式。
首先,我们先谈谈浅克隆的实现
一、浅克隆
Java中实现浅克隆主要就是要实现Cloneable接口,然后返回克隆对象。
假设,现在我们有两个类,账户类Account和账户详情类AccountDetail,代码如下:
/**
* 类名 Account
* 描述 账户类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Cloneable{
/** 主键 */
private Long id;
/** 账户名称 */
private String name;
/** 账户详情 */
private AccountDetail detail;
/** 重写clone方法,改为public方便外部调用 */
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/**
* 类名 AccountDetail
* 描述 账户详情类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AccountDetail {
/** 账户ID */
private Long accountId;
/** 邮箱 */
private String email;
}
账户类引用了账户详情类,如果我们仅仅是浅克隆,那么详情类将不会真正被拷贝,而是拷贝了它的引用。
测试代码:
/**
* 类名 CloneTest
* 描述 克隆测试类
*/
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
// 浅克隆证明
AccountDetail detail = new AccountDetail(1L, "[email protected]");
Account account = new Account(1L, "小何", detail);
// 克隆
Account clone = (Account) account.clone();
// 判断详情对对象是否相同,预期值(true)
System.out.println(clone.getDetail() == account.getDetail());
}
}
上述代码执行后,结果为true, 表示原始对象和克隆对象的AccountDetail是同一个 引用。也就是说,该对象没有被克隆。如果修改了AccountDetail对象的值,原始对象和克隆对象都将会发生变化。有时候,这可能并不是我们希望看到的。
所以,我们需要连对象里面的对象也要是一个新的对象。每一个属性都被完全拷贝,这才是深克隆。
二、深克隆
1、手动为引用属性赋值
我们修改Account对象的clone方法
@Override
public Object clone() throws CloneNotSupportedException {
Account account = (Account) super.clone();
// 手动赋值实现深克隆
account.setDetail(this.getDetail());
return account;
}
然后重新运行上述的测试代码,得出的结论为false,也就是原始对象和克隆对象的AccountDetail是不同的对象。也就实现了深克隆。
显然,这种手动的方式在关联对象少的情况是可取的,假设关联的对象里面也包含了对象,就需要层层修改,比较麻烦。不推荐这样使用!
于是,有没有简单的方式呢。当然有,继续往下看!
2、使用fastJson
这种方式我们我们依赖于阿里巴巴的fastJSON包。
public static void main(String[] args) {
AccountDetail detail = new AccountDetail(1L, "[email protected]");
Account account = new Account(1L, "小何", detail);
// fastJson实现克隆
Account clone = JSONObject.parseObject(JSONObject.toJSONBytes(account), Account.class);
// 判断详情对对象是否相同,预期值(false)
System.out.println(clone.getDetail() == account.getDetail());
}
这种方式和很简单 ,也不需要事先Clonable接口 ,不失为一种好的解决方式。
3、使用ObjectStream(推荐)
这就是使用 Java流中的序列化对象事先深克隆。
奉上一个工具类
/**
* 类名 SerialCloneUtils
* 描述 序列化克隆工具
*
* @author hedonglin
* @version 1.0
* @date 2019/11/15 16:36
*/
public class SerialCloneUtils{
/**
* 使用ObjectStream序列化实现深克隆
* @return Object obj
*/
public static T deepClone(T t) throws CloneNotSupportedException {
// 保存对象为字节数组
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try(ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(t);
}
// 从字节数组中读取克隆对象
try(InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return (T)(in.readObject());
}
}catch (IOException | ClassNotFoundException e){
CloneNotSupportedException cloneNotSupportedException = new CloneNotSupportedException();
e.initCause(cloneNotSupportedException);
throw cloneNotSupportedException;
}
}
}
可以直接通过工具类来实现克隆,跟上面fastJson使用类似,当然也可以再升级一下,提供一个公共的父类,纪要继承该类就可以实现深克隆。
/**
* 类名 SerialClone
* 描述 序列化克隆类,只有继承该类,就可以实现深克隆
*
*/
public class SerialClone implements Cloneable, Serializable {
private static final long serialVersionUID = 5794148504376232369L;
@Override
public Object clone() throws CloneNotSupportedException {
return SerialCloneUtils.deepClone(this);
}
}
改造Account类和AccountDetail类,都继承SerialClone。
/**
* 类名 Account
* 描述 账户类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Account /*implements Cloneable*/ extends SerialClone {
private static final long serialVersionUID = 320551237720888204L;
/** 主键 */
private Long id;
/** 账户名称 */
private String name;
/** 账户详情 */
private AccountDetail detail;
// @Override
// public Object clone() throws CloneNotSupportedException {
// Account account = (Account) super.clone();
// // 手动赋值实现深克隆
// account.setDetail(this.getDetail());
// return account;
// }
}
/**
* 类名 AccountDetail
* 描述 账户详情类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AccountDetail extends SerialClone {
private static final long serialVersionUID = -2232096703114704249L;
/** 账户ID */
private Long accountId;
/** 邮箱 */
private String email;
}
重新运行测试代码,返回值就已经变成了false。
这种方式,代码虽然多一点,但确实比较高效和容易抽象的,也是很常用的方式!
闲暇之余,记录一下代码!码农就是需要不断学习,不进则退,加油!