Deep and shallow cloning of Java 8

References:

What is the difference between deep cloning and shallow cloning? How are they realized

catalogue

1, Introduction to deep cloning and shallow cloning

1. Overview

2. Implementation of copy function

2, Implementation of deep cloning

1. Clone all objects

2. Deep cloning is realized by construction method

3. Deep cloning through byte stream

       4. Use Open Source Toolkit

Supplement:

1. Why should we inherit clonable interface and rewrite clone method

2. What are the requirements for rewriting the clone() method

1, Introduction to deep cloning and shallow cloning

1. Overview

java allows an object to be assigned exactly the same object, which is called cloning. Cloning is divided into shallow cloning and deep cloning. The difference between them is that for member variables of reference type, shallow cloning copies references while deep cloning copies objects.

Shadow Clone is to copy the attributes of the prototype object whose member variables are value types to the clone object, and copy the reference address of the prototype object whose member variables are reference types to the clone object, that is, if there are member variables in the prototype object as reference objects, the address of the reference object is shared with the prototype object and the clone object.

In short, shallow cloning only copies the prototype object, but does not copy the object it refers to.

Deep Clone copies all types in the prototype object, whether value type or reference type, to the cloned object, that is, Deep Clone copies both the prototype object and the object referenced by the prototype object to the cloned object.

 

2. Implementation of copy function

To implement cloning, you need to inherit the clonable interface and override the clone() method in the Object class.

public class ProblemSolution {
    public static void main(String[] args)  {
        // Create assigned object
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // Clone p1 object
        People p2;
        try {
            p2 = (People) p1.clone();
            System.out.println(p1.getName().equals(p2.getName()));
            System.out.println(p1.getName()==p2.getName());
        }
        catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

class People implements Cloneable {
    // attribute
    private Integer id;
    private String name;
    /**
     * Override clone method
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

The results are as follows:

true
true

The above code uses the clone method of the Object class to implement the clone. We can see from the results that the String type reference in the two objects points to the same Object, so the clone method of the Object is a shallow copy.

2, Implementation of deep cloning

1. Clone all objects

In this way, we need to modify the People and Address classes, make them inherit the clonable interface and rewrite the clone method, so that all reference objects can be cloned, so as to realize the deep cloning of the People class.

class People implements Cloneable {
    private Address address;
    // Override the clone method to clone every member variable internally
    @Override
    protected Object clone() throws CloneNotSupportedException {
        People people = (People) super.clone();
        people.setAddress((Address)this.address.clone()); // Reference type clone assignment
        return people;
    }
    public void setAddress(Address address) {
        this.address = address;       
    }
    public Address getAddress() {
        return address;
    }
}

class Address implements Cloneable { 
    private String city;
    // Override clone method
    @Override
    protected Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
    public Address(String city){
        this.city = city;
    }

    // Override the equals method
    @Override
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof Address) {
            Address anotherAddress = (Address)anObject;
            return this.getCity().equals(anotherAddress.getCity());
        }
        return false;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
}

Compare p1 with p2 from clone.

    public static void main(String[] args)  {
        // Create assigned object
        People p1 = new People();
        p1.setAddress(new Address("Beijing"));
        // Clone p1 object
        People p2;
        try {
            p2 = (People) p1.clone();
            System.out.println(p1.getAddress().equals(p2.getAddress()));
            System.out.println(p1.getAddress()==p2.getAddress());
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

The results are as follows:

true
false

We can see that the member variable address in p1 and p2 are different objects, and the deep cloning is successful. But the problem is that all classes need to implement clone method, which is too cumbersome.

        

2. Deep cloning is realized by construction method

In Effective Java, it is recommended to use a constructor to implement deep cloning. If the constructor parameter is a basic data type or string type, it will be assigned directly. If it is an object type, it needs to create a new object.

    public static void main(String[] args)  {
        // Create assigned object
        People p1 = new People(new Address("Beijing"));
        People p2 = new People(new Address(p1.getAddress().getCity()));
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

The results are as follows:

true
false

3. Deep cloning through byte stream

The way to realize deep cloning through the byte stream of JDK is to first write the prototype object to the byte stream in memory, and then read the just stored information from the byte stream to return as a new object. Then the new object and the prototype object do not share any address, so deep cloning is realized.

To put it simply, you can use serialized storage and then read it to obtain a completely consistent object.

class Address implements Serializable{
    ...
}

class People implements Serializable{
    ...
}

class StreamClone {
    public static <T extends Serializable> T clone(People obj) {
        T cloneObj = null;
        try {
            // Write byte stream
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bo);
            oos.writeObject(obj);
            oos.close();
            // Allocate memory, write the original object and generate a new object
            ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//Get the above output byte stream
            ObjectInputStream oi = new ObjectInputStream(bi);
            // Returns the generated new object
            cloneObj = (T) oi.readObject();
            oi.close();
        } catch (Exception e) {

            e.printStackTrace();
        }
        return cloneObj;
    }
}
    public static void main(String[] args)  {
        // Create assigned object
        People p1 = new People(new Address("Beijing"));
        People p2 = StreamClone.clone(p1);
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

The results are as follows:

true
false

It should be noted that since the deep cloning is realized through byte stream serialization, each object must be able to be serialized. The Serializable interface must be implemented to identify that it can be serialized. Otherwise, NotSerializableException will be thrown.

4. Use Open Source Toolkit

Taking fastjson as an example, the use of JSON tool classes will first convert the object into a string, and then convert it from a string into a new object. Because the new object is converted from a string, it will not have any association with the prototype object. In this way, deep cloning is realized, and the implementation method of other similar JSON tool classes is the same.

    public static void main(String[] args)  {
        // Create assigned object
        People p1 = new People(new Address("Beijing"));
        Gson gson = new Gson();
        People p2 = gson.fromJson(gson.toJson(p1), People.class);
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

        

Supplement:

1. Why should we inherit clonable interface and rewrite clone method

There is no direct answer to why cloning should be designed like this. We can only try to answer this question with some experience and source code documents.

From the source code, we can see that clonable interface has existed in JDK 1.0, so there have been cloning methods since then. How can we identify that a class level object has cloning methods? Although cloning is important, we cannot add cloning to every class by default, which is obviously inappropriate. There are only a few methods we can use:

  • Add an identifier on the class, which is used to declare that a class has the function of cloning, just like the final keyword;
  • Using annotations in Java;
  • Implement an interface;
  • Inherit a class.

First, it is obviously inappropriate to add a new class ID for an important but infrequent cloning function; The second one is that the cloning function appeared relatively early, and there was no annotation function at that time, so it can not be used; The third point basically meets our needs. The fourth point is similar to the first point. A base class needs to be sacrificed for a cloning function, and Java can only inherit alone, so this scheme is not suitable. Using the exclusion method, there is no doubt that the way to implement interfaces was the most reasonable scheme at that time, and in the Java language, a class can implement multiple interfaces.

So why add a clone() method to the Object?

Because the clone() method requires high time and efficiency, it's best to have direct support from the JVM. Since you want direct support from the JVM, you need to find an API to expose this method. The most direct way is to put it into a base class Object of all classes, so that all classes can easily call it.

2. What are the requirements for rewriting the clone() method

First, look at the source code. clone() is a local method decorated with native, so the execution performance will be very high, and its return type is Object. Therefore, after calling cloning, you need to forcibly convert the Object to the target type. Although we can't see the concrete implementation of the clone() method, we can see some information from the comments.

/**
 * Creates and returns a copy of this object.  The precise meaning
 * of "copy" may depend on the class of the object. The general
 * intent is that, for any object {@code x}, the expression:
 * <blockquote>
 * <pre>
 * x.clone() != x</pre></blockquote>
 * will be true, and that the expression:
 * <blockquote>
 * <pre>
 * x.clone().getClass() == x.getClass()</pre></blockquote>
 * will be {@code true}, but these are not absolute requirements.
 * While it is typically the case that:
 * <blockquote>
 * <pre>
 * x.clone().equals(x)</pre></blockquote>
 * will be {@code true}, this is not an absolute requirement.
 * <p>
 * By convention, the returned object should be obtained by calling
 * {@code super.clone}.  If a class and all of its superclasses (except
 * {@code Object}) obey this convention, it will be the case that
 * {@code x.clone().getClass() == x.getClass()}.
 * <p>
 * By convention, the object returned by this method should be independent
 * of this object (which is being cloned).  To achieve this independence,
 * it may be necessary to modify one or more fields of the object returned
 * by {@code super.clone} before returning it.  Typically, this means
 * copying any mutable objects that comprise the internal "deep structure"
 * of the object being cloned and replacing the references to these
 * objects with references to the copies.  If a class contains only
 * primitive fields or references to immutable objects, then it is usually
 * the case that no fields in the object returned by {@code super.clone}
 * need to be modified.
 * <p>
 * ......
 */
protected native Object clone() throws CloneNotSupportedException;

As can be seen from the notes:

(1) for all objects, X. clone()= X should return true because the cloned object is not the same object as the original object;

(2) for all objects, x.clone() Getclass() = = X. getclass() should return true because the type of the cloned object is the same as that of the original object;

(3) for all objects, x.clone() Equals (x) should return true because their values are the same when using equals comparison.

Keywords: Java

Added by TheCase on Thu, 10 Feb 2022 21:52:47 +0200