【基础面试题】深入理解深拷贝和浅拷贝

【基础面试题】深入理解深拷贝和浅拷贝,第1张

【基础面试题】深入理解深拷贝和浅拷贝

文章目录
  • 数据类型
  • 赋值语句
  • 深拷贝和浅拷贝
    • 概念理解
    • 不可变对象
    • Cloneable接口
      • 只含有基础类型和不可变类型时
      • 含有引用类型时
      • 解决引用类型的问题
      • 问题延伸
      • 最终的解决方式
        • 字节流
        • 重新实现
    • 总结

数据类型

在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,只要有一个属性发生了变化,两个对象就都会改变,这通常不是我们希望看到的结果。

基础类型的赋值,实际上在栈中是两个对象

而引用类型的赋值,实际上只是在引用上做了处理,实际在堆中的对象还是只有一个

深拷贝和浅拷贝 概念理解
  • 浅拷贝:如果是基础类型,则直接拷贝数值,然后赋值给新的对象,如果是引用类型,则只复制引用,并不复制数据本身。
  • 深拷贝:如果是基础类型,和浅拷贝一样,如果是用引用类型,则不是只复制引用,还会复制数据本身。

深拷贝

不可变对象

有一类对象比较特殊,它们虽然是引用类型对象,但依然可以保证浅拷贝后,得到就是你想要的对象,那就是不可变对象。

比如像下面这样,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;
    }
}

对于不可变的类,就算直接赋值了又能怎么样,反正你也无法再修改它了,所以它是安全的。

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;

    
    @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;

    
    @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;

    
    @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 roleMap;
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.roleMap = (HashMap) roleMap.clone();
        return user;
    }
}
public static void main(String[] args) throws CloneNotSupportedException {
    User u1 = new User();
    
    HashMap roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = (User) u1.clone();
	HashMap 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提供的克隆方法本身就是浅拷贝。。。

 
 @SuppressWarnings("unchecked")
 @Override
 public Object clone() {
     HashMap result;
     try {
         result = (HashMap)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 roleMap;

    public static  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 roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = User.clone(u1);
    HashMap 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=系统管理员)})
重新实现

实际上你可以自己实现一套clone方法,给它定义为拷贝工厂,或者使用一些已经实现好的第三方工具类。

比如org.springframework.beans包下提供的BeanUtils类

public static void main(String[] args) {
    User u1 = new User();
    HashMap roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = new User();
    // 使用copyProperties方法
    BeanUtils.copyProperties(u1,u2);
    HashMap roleMap2 = u2.getRoleMap();
    Role role = roleMap2.get("A");
    role.setRoleName("普通员工");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}

Hutool工具包

// 命名几乎和spring的一样
BeanUtil.copyProperties(u1, u2);
总结

实际上你应该已经发现了,虽然Object类为我们提供了clone方法,但有时候并不能很好的使用它,可能需要多层级的逐个克隆,甚至如果添加了某个引用对象时,忘了修改clone方法还会带来一些奇怪的问题,也许我们应该永远不去使用它,而是通过其他的方式来替代。

截自阿里Java开发手册

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/zaji/5676760.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存