数据的存储
在理解深拷贝和浅拷贝之前,我们需要先了解数据的存储方式。
数据类型
在Java中数据的存储方式和数据类型相关,数据类型可以分为:基础类型和引用类型,其中当基础类型为全局变量时存储在栈中,为局部变量时存储在堆中,不过无论是在栈中还是在堆中,存储的都是具体的数值,与之不同的引用类型,则记录的是地址,然后通过指针引用的方式指向具体的内存区域。
比如在m
这个方法中,用到的基础类型a
与引用类型User
在内存中的存储就如下图所示:
public void m(){
int a = 128;
User user = new User();
}
赋值语句
在应用程序中对象拷贝一般都可以通过赋值语句来实现,比如像下面这样:
int a = 128;
int b = a;
可以认为,b
拷贝了a
对于基础类型来说,这样是没有问题的,但对于引用类型就有问题了,比如像下面这样:
User user1 = new User();
User user2 = user1;
无论是user1
还是user2
,只要有一个属性发生了变化,两个对象就都会改变,这通常不是我们希望看到的结果。
基础类型的赋值,实际上在栈中是两个对象。
而引用类型的赋值,实际上只是在引用上做了处理,实际在堆中的对象还是只有一个。
深拷贝和浅拷贝
概念理解
- 浅拷贝:如果是基础类型,则直接拷贝数值,然后赋值给新的对象,如果是引用类型,则只复制引用,并不复制数据本身。
- 深拷贝:如果是基础类型,和浅拷贝一样,如果是用引用类型,则不是只复制引用,还会复制数据本身。
深拷贝
深拷贝时user2会在堆中新开辟一块空间。
不可变对象
有一类对象比较特殊,它们虽然是引用类型对象,但依然可以保证浅拷贝后,得到就是你想要的对象,那就是不可变对象。
比如像下面这样,str1
和str2
两个对象是不会互相影响的。
String str1 = "a";
String str2 = str1;
或者是这样的类
final class User {
final String name;
final String age;
public User(String name, String age) {
this.name = name;
this.age = age;
}
}
对于不可变的类,就算直接赋值了又能怎么样,反正你也无法再修改它了,所以它是安全的。
// u1、u2都不能修改
User u1 = new User("小明", "18");
User u2 = u1;
Cloneable接口
实际上JDK也为我们提供了对象clone
的方法,就是实现Cloneable
接口,只要实现了这个接口的类就表明该对象具有允许clone
的能力,Cloneable
接口本身不包含任何方法,它只是决定了Object
中受保护的clone
方法实现的行为:
如果一个类实现了Cloneable
接口,Object
的clone
方法就返回该对象的拷贝,否则就抛出java.lang.CloneNotSupportedException
异常。
@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
private String name;
private int age;
/**
* 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
如果你认为一个类实现了Cloneable
接口,并且调用super.clone()
方法就能够得到你想要的对象,那你就错了,因为super.clone()
方法就和浅拷贝一样,如果克隆的对象中包含可变的引用类型,实际上是存在问题的。
只含有基础类型和不可变类型时
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User("小明", 18);
User u2 = (User) u1.clone();
u2.setName("小王");
u2.setAge(20);
u1.setName("小红");
u1.setAge(19);
log.info("u1:{}", u1);
log.info("u2:{}", u2);
}
因为User
对象只有基础类型int
和不可变类型String
,所以直接调用spuer.clone()
方法没有问题
u1:User(name=小红, age=19)
u2:User(name=小王, age=20)
含有引用类型时
现在我们为User
对象新增一个Role
的属性
@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
private String name;
private int age;
private Role[] roles;
/**
* 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
*
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Role{
private String roleName;
}
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User();
u1.setName("小明");
u1.setAge(18);
Role[] roles = new Role[2];
roles[0] = new Role("A系统管理员");
roles[1] = new Role("B系统普通员工");
u1.setRoles(roles);
log.info("u1:{}", u1);
User u2 = (User) u1.clone();
u2.setName("小王");
u2.setAge(20);
Role[] roles2 = u2.getRoles();
roles2[0] = new Role("A系统普通员工");
roles2[1] = new Role("B系统管理员");
u2.setRoles(roles2);
log.info("u1:{}", u1);
}
问题出现了,我只修改了克隆出来的u2
对象,但是u1
对象也没改变了。
u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])
u1:User(name=小明, age=18, roles=[Role(roleName=A系统普通员工), Role(roleName=B系统管理员)])
解决引用类型的问题
典型的浅拷贝的问题,那么要解决这个问题也很简单,改成下面这样即可
@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
private String name;
private int age;
private Role[] roles;
/**
* 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
*
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.roles = roles.clone();
return user;
}
}
此时再执行,结果就正确了。
u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])
u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])
问题延伸
实际上在有些的情况下,上面的处理方式还是存在问题,比如像下面这样
现在对象是HashMap
了
@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
private HashMap<String, Role> roleMap;
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.roleMap = (HashMap<String, Role>) roleMap.clone();
return user;
}
}
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User();
HashMap<String, Role> roleMap1 = new HashMap<>();
roleMap1.put("A", new Role("系统管理员"));
u1.setRoleMap(roleMap1);
log.info("u1:{}", u1);
User u2 = (User) u1.clone();
HashMap<String, Role> roleMap2 = u2.getRoleMap();
Role role = roleMap2.get("A");
role.setRoleName("普通员工");
roleMap2.put("A", role);
u2.setRoleMap(roleMap2);
log.info("u1:{}", u1);
}
u1:User(roleMap={A=Role(roleName=系统管理员)})
u1:User(roleMap={A=Role(roleName=普通员工)})
为什么不行呢?因为HashMap
提供的克隆方法本身就是浅拷贝。
/**
* Returns a shallow copy of this <tt>HashMap</tt> instance: the keys and
* values themselves are not cloned.
*
* @return a shallow copy of this map
*/
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
最终的解决方式(字节流)
你在百度上很容易查询到解决方式,最常见的就是字节流。
比如像下面这样。
@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Serializable {
private HashMap<String, Role> roleMap;
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(obj);
outputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
cloneObj = (T) inputStream.readObject();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Role implements Serializable {
private String roleName;
}
此时再调用就没有问题了。
public static void main(String[] args) {
User u1 = new User();
HashMap<String, Role> roleMap1 = new HashMap<>();
roleMap1.put("A", new Role("系统管理员"));
u1.setRoleMap(roleMap1);
log.info("u1:{}", u1);
User u2 = User.clone(u1);
HashMap<String, Role> roleMap2 = u2.getRoleMap();
Role role = roleMap2.get("A");
role.setRoleName("普通员工");
roleMap2.put("A", role);
u2.setRoleMap(roleMap2);
log.info("u1:{}", u1);
}
u1:User(roleMap={A=Role(roleName=系统管理员)})
u1:User(roleMap={A=Role(roleName=系统管理员)})
实际上你应该已经发现了,虽然Object
类为我们提供了clone
方法,但有时候并不能很好的使用它,可能需要多层级的逐个克隆,甚至如果添加了某个引用对象时,忘了修改clone
方法还会带来一些奇怪的问题,也许我们应该永远不去使用它,而是通过其他的方式来替代。
注意工具类的使用
最后,我要提醒一下,我知道现在实际业务处理中经常会用到例如像:org.springframework.beans
包下提供的BeanUtils
类或者Hutool
工具包等这样的工具类,大多数情况下类似这样的工具类都只是浅拷贝,请在业务中使用时注意是否需要关心。