[basic interview questions] in depth understanding of deep copy and shallow copy

Posted by minc on Fri, 17 Dec 2021 03:07:45 +0100

data type

In Java, data types can be divided into basic types and reference types. When the basic type is a global variable, it is stored in the stack, and when it is a local variable, it is stored in heap memory. Whether it is stored in the stack or heap, it is a specific value. For different reference types, it records an address, and then points to a specific memory area by reference.

For example, in m this method, the basic type a and reference type User used are stored in memory as shown in the following figure

public void m(){
	int a = 128;
	User user = new User();
}

Assignment statement

In an application, object copying can generally be realized through assignment statements, such as the following

int a = 128;
int b = a;

It can be considered that b copies a

For basic types, this is no problem, but for reference types, such as the following

User user1 = new User();
User user2 = user1;

Whether user1 or user2, as long as one attribute changes, both objects will change, which is usually not the result we want to see.

The assignment of the basic type is actually two objects in the stack

In fact, the assignment of reference type is only processed on the reference, and there is only one object in the heap

Deep and shallow copies

Conceptual understanding

  • Shallow copy: if it is a basic type, copy the value directly and assign it to a new object. If it is a reference type, only the reference is copied, not the data itself.
  • Deep copy: if it is a basic type, like shallow copy, if it is a reference type, it will not only copy the reference, but also copy the data itself.

Deep copy

Immutable object

There is a special class of objects. Although they are reference type objects, they can still ensure that after shallow copy, you get the object you want, that is, immutable objects.

For example, as shown below, str1 and str2 objects will not affect each other.

String str1 = "a";
String str2 = str1;

Or such a class

final class User {
    final String name;
    final String age;
    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

For an immutable class, even if it is directly assigned, you can't modify it anyway, so it's safe.

User u1 = new User("Xiao Ming", "18");
User u2 = u1;

Clonable interface

In fact, the JDK also provides us with methods for the Object clone, that is, to implement the Cloneable interface. As long as the class that implements this interface indicates that the Object has the ability to allow clone. The Cloneable interface itself does not contain any methods, but only determines the behavior of the protected clone method implementation in the Object:

If a class implements the clonable interface, the clone method of the Object returns a copy of the Object, otherwise it throws a Java Lang. clonenotsupportedexception exception.

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;

    /**
     * If the clonable interface is not implemented, call super The clone () method throws an exception
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

If you think a class implements the clonable interface and calls super The clone () method can get the object you want, and you are wrong because super The clone () method is the same as the shallow copy. If the cloned object contains variable reference types, there is actually a problem.

When only basic and immutable types are included

public static void main(String[] args) throws CloneNotSupportedException {
	User u1 = new User("Xiao Ming", 18);
	User u2 = (User) u1.clone();
	
	u2.setName("Xiao Wang");
	u2.setAge(20);
	
	u1.setName("Xiao Hong");
	u1.setAge(19);
	
	log.info("u1:{}", u1);
	log.info("u2:{}", u2);
}

Because the User object has only the base type int and the immutable type String, Spuer. Is called directly There is no problem with the clone () method

u1:User(name=Xiao Hong, age=19)
u2:User(name=Xiao Wang, age=20)

With reference type

Now we add a Role attribute to the User object

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;
    private Role[] roles;

    /**
     * If the clonable interface is not implemented, call super The clone () method throws an exception
     *
     * @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("Xiao Ming");
    u1.setAge(18);
    
    Role[] roles = new Role[2];
    roles[0] = new Role("A system administrator");
    roles[1] = new Role("B System general staff");
    u1.setRoles(roles);
    log.info("u1:{}", u1);
    
    User u2 = (User) u1.clone();
    u2.setName("Xiao Wang");
    u2.setAge(20);
    
    Role[] roles2 = u2.getRoles();
    roles2[0] = new Role("A System general staff");
    roles2[1] = new Role("B system administrator");
    u2.setRoles(roles2);
    
    log.info("u1:{}", u1);
}

The problem occurred. I only modified the cloned u2 object, but the u1 object did not change.

u1:User(name=Xiao Ming, age=18, roles=[Role(roleName=A system administrator), Role(roleName=B System general staff)])
u1:User(name=Xiao Ming, age=18, roles=[Role(roleName=A System general staff), Role(roleName=B system administrator)])

Solve the problem of reference type

For the typical shallow copy problem, it is also very simple to solve this problem. Change it to the following

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;
    private Role[] roles;

    /**
     * If the clonable interface is not implemented, call super The clone () method throws an exception
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.roles = roles.clone();
        return user;
    }
}

Execute again at this time, and the result is correct.

u1:User(name=Xiao Ming, age=18, roles=[Role(roleName=A system administrator), Role(roleName=B System general staff)])
u1:User(name=Xiao Ming, age=18, roles=[Role(roleName=A system administrator), Role(roleName=B System general staff)])

Problem extension

In fact, in some cases, the above processing method still has problems, such as the following

Now the object is 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("system administrator"));
    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("Ordinary staff");
	roleMap2.put("A", role);
	u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}
u1:User(roleMap={A=Role(roleName=system administrator)})
u1:User(roleMap={A=Role(roleName=Ordinary staff)})

Why not? Because the cloning method provided by HashMap itself is a shallow copy...

 /**
  * 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;
 }

Final solution

Byte stream

You can easily find the solution on Baidu. The most common is byte stream.

Like this.

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

There is no problem calling again at this time.

public static void main(String[] args) {
    User u1 = new User();
    HashMap<String, Role> roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("system administrator"));
    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("Ordinary staff");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}
u1:User(roleMap={A=Role(roleName=system administrator)})
u1:User(roleMap={A=Role(roleName=system administrator)})

Re implementation

In fact, you can implement a set of clone methods, define it as a copy factory, or use some implemented third-party tool classes.

Like org springframework. BeanUtils class provided under beans package

public static void main(String[] args) {
    User u1 = new User();
    HashMap<String, Role> roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("system administrator"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = new User();
    // Using the copyProperties method
    BeanUtils.copyProperties(u1,u2);
    HashMap<String, Role> roleMap2 = u2.getRoleMap();
    Role role = roleMap2.get("A");
    role.setRoleName("Ordinary staff");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}

Hutool Toolkit

// The name is almost the same as spring
BeanUtil.copyProperties(u1, u2);

summary

In fact, you should have found that although the Object class provides us with the clone method, sometimes it can not be used well. It may need to be cloned one by one at multiple levels. Even if you forget to modify the clone method when adding a reference Object, it will bring some strange problems. Maybe we should never use it, but replace it in other ways.

From Alibaba Java development manual

Topics: Java