What's the difference between deep clone and shallow clone? What are the ways to realize it?

Keywords: Java JSON JDK Apache

Cloning can be used to quickly build a copy of an existing object, which is part of the Java foundation and one of the frequently asked knowledge points in an interview.

What are shallow cloning and deep cloning? How to achieve cloning?

Typical answer

Shadow Clone is to copy all the attributes in the prototype object with member variable of value type to the clone object, and copy the reference address of the prototype object with member variable of reference type to the clone object, that is to say, if any member variable in the prototype object is a reference object, the address of the reference object is shared to the prototype object and the clone object.

In short, shallow cloning only copies the prototype object, but not the objects it references, as shown in the following figure:

Deep Clone copies all types in the prototype object, whether value type or reference type, to the clone object. That is to say, Deep Clone copies both the prototype object and the object referenced by the prototype object to the clone object, as shown in the following figure:

To realize cloning in Java language, it is necessary to implement the clonable interface and rewrite the clone() method in the Object class. The implementation code is as follows:

public class CloneExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // Create assigned object
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // Clone p1 object
        People p2 = (People) p1.clone();
        // Print name
        System.out.println("p2:" + p2.getName());
    }
    static 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 of the above procedures are as follows:

p2:Java

Examination point analysis

Cloning related interview questions is not too difficult, but it is easy to be ignored because of the low frequency of use. Interviewers usually ask this knowledge point when they are on one or two sides. The interview questions related to cloning are as follows:

  • At java.lang.Object What are the conventions for the clone() method in?

  • Arrays.copyOf() deep clone or shallow clone?

  • How to realize deep cloning?

  • Why should Java clone be designed to not only implement the null interface Cloneable, but also rewrite the Object's clone() method?

Knowledge expansion

clone() source code analysis
To really understand cloning, we should start with its source code, which is as follows:

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

From the above source code annotation information, we can see that there are three conventions for Object to clone() method:

For all objects, x.clone()! = x should return true, because the cloned object and the original object are not the same object;

For all objects, x.clone().getClass() == x.getClass() should return true, because the clone object is the same type as the original object;

For all objects, x.clone().equals(x) should return true because when using equals comparisons, their values are the same.

In addition to the annotation information, we look at the implementation method of clone(). We find that clone() is a native decorated local method, so its performance will be very high, and its return type is Object, so we need to force the Object to the target type after calling clone.

Arrays.copyOf()

If it is an array type, we can directly use Arrays.copyOf() to achieve cloning, the implementation code is as follows:

People[] o1 = {new People(1, "Java")};
People[] o2 = Arrays.copyOf(o1, o1.length);
// Modify the value of the first element of the prototype object
o1[0].setName("Jdk");
System.out.println("o1:" + o1[0].getName());
System.out.println("o2:" + o2[0].getName());

The execution results of the above procedures are as follows:

o1:Jdk
o2:Jdk

From the results, we can see that after modifying the first element of the cloned object, the first element of the prototype object is also modified, which shows that Arrays.copyOf() is actually a shallow clone.

Because arrays are special arrays, which are reference types, they are used in Arrays.copyOf() In fact, I just copied a copy of the reference address to the cloned object. If I modify its reference object, all the objects pointing to it (reference address) will change. Therefore, the result is that the first element of the cloned object is modified, and the prototype object is also modified.

Summary of deep clone implementation methods
There are many ways to realize deep cloning, which can be roughly divided into the following categories:

  • All objects implement the clone method;

  • Deep clone is realized by constructing method;

  • The byte stream of JDK is used to realize deep clone;

  • Use third-party tools to implement deep cloning, such as Apache Commons Lang;

  • Use JSON tool classes to implement deep cloning, such as Gson, FastJSON, etc.

Next, we implement the above methods respectively. Before we start, we define a public user class. The code is as follows:

/**
 * User class
 */
public class People {
    private Integer id;
    private String name;
    private Address address; // Include Address reference object
    // Ignore constructor, set, get methods
}
/**
 * Address class
 */
public class Address {
    private Integer id;
    private String city;
    // Ignore constructor, set, get methods
}

You can see that there is a reference object Address in the People object.

1. Clone all objects

In this way, we need to modify the People and Address classes to implement the clonable interface and clone all reference objects, so as to realize the deep cloning of the People class. The code is as follows:

public class CloneExample {
    public static void main(String[] args) throws CloneNotSupportedException {
          // Create assigned object
          Address address = new Address(110, "Beijing");
          People p1 = new People(1, "Java", address);
          // Clone p1 object
          People p2 = p1.clone();
          // Modifying prototype objects
          p1.getAddress().setCity("Xi'an");
          // Output p1 and p2 address information
          System.out.println("p1:" + p1.getAddress().getCity() +
                  " p2:" + p2.getAddress().getCity());
    }
    /**
     * User class
     */
    static class People implements Cloneable {
        private Integer id;
        private String name;
        private Address address;
        /**
         * Override clone method
         * @throws CloneNotSupportedException
         */
        @Override
        protected People clone() throws CloneNotSupportedException {
            People people = (People) super.clone();
            people.setAddress(this.address.clone()); // Reference type clone assignment
            return people;
        }
        // Ignore constructor, set, get methods
    }
    /**
     * Address class
     */
    static class Address implements Cloneable {
        private Integer id;
        private String city;
        /**
         * Override clone method
         * @throws CloneNotSupportedException
         */
        @Override
        protected Address clone() throws CloneNotSupportedException {
            return (Address) super.clone();
        }
        // Ignore constructor, set, get methods
    }
}

The execution results of the above procedures are as follows:

p1: Xi'an p2: Beijing

It can be seen from the results that when we modify the reference property of the prototype object, it does not affect the cloned object, which indicates that the object has achieved deep cloning.

2. Deep cloning by construction method

In Effective Java, it is recommended to use the Copy Constructor to realize deep cloning. If the parameters of the constructor are basic data type or string type, they are assigned directly. If they are object type, an object needs to be re new ed. The implementation code is as follows:

public class SecondExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // create object
        Address address = new Address(110, "Beijing");
        People p1 = new People(1, "Java", address);

        // Call constructor to clone object
        People p2 = new People(p1.getId(), p1.getName(),
                new Address(p1.getAddress().getId(), p1.getAddress().getCity()));

        // Modifying prototype objects
        p1.getAddress().setCity("Xi'an");

        // Output p1 and p2 address information
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * User class
     */
    static class People {
        private Integer id;
        private String name;
        private Address address;
        // Ignore constructor, set, get methods
    }

    /**
     * Address class
     */
    static class Address {
        private Integer id;
        private String city;
        // Ignore constructor, set, get methods
    }
}

The execution results of the above procedures are as follows:

p1: Xi'an p2: Beijing

It can be seen from the results that when we modify the reference property of the prototype object, it does not affect the cloned object, which indicates that the object has achieved deep cloning.

3. Deep clone by 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 out the information just stored 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. The code is as follows:

import java.io.*;

public class ThirdExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // create object
        Address address = new Address(110, "Beijing");
        People p1 = new People(1, "Java", address);

        // Clone by byte stream
        People p2 = (People) StreamClone.clone(p1);

        // Modifying prototype objects
        p1.getAddress().setCity("Xi'an");

        // Output p1 and p2 address information
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * Clone by byte stream
     */
    static 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 original object, generate new object
                ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//Get the output byte stream above
                ObjectInputStream oi = new ObjectInputStream(bi);
                // Return the generated new object
                cloneObj = (T) oi.readObject();
                oi.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return cloneObj;
        }
    }

    /**
     * User class
     */
    static class People implements Serializable {
        private Integer id;
        private String name;
        private Address address;
        // Ignore constructor, set, get methods
    }

    /**
     * Address class
     */
    static class Address implements Serializable {
        private Integer id;
        private String city;
        // Ignore constructor, set, get methods
    }
}

The execution results of the above procedures are as follows:

p1: Xi'an p2: Beijing

In this way, it should be noted that since it is a deep clone realized by byte stream serialization, each object must be Serializable, and the Serializable interface must be implemented to identify that it can be serialized, otherwise an exception will be thrown( java.io.NotSerializableException).

4. Deep cloning through third-party tools

In this lesson, Apache Commons Lang is used to realize deep cloning. The implementation code is as follows:

import org.apache.commons.lang3.SerializationUtils;

import java.io.Serializable;

/**
 * Way 4 of deep cloning: through apache.commons.lang  realization
 */
public class FourthExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // create object
        Address address = new Address(110, "Beijing");
        People p1 = new People(1, "Java", address);

        // Call apache.commons.lang  Clone object
        People p2 = (People) SerializationUtils.clone(p1);

        // Modifying prototype objects
        p1.getAddress().setCity("Xi'an");

        // Output p1 and p2 address information
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * User class
     */
    static class People implements Serializable {
        private Integer id;
        private String name;
        private Address address;
        // Ignore constructor, set, get methods
    }

    /**
     * Address class
     */
    static class Address implements Serializable {
        private Integer id;
        private String city;
        // Ignore constructor, set, get methods
    }
}

The execution results of the above procedures are as follows:

p1: Xi'an p2: Beijing

It can be seen that this method is similar to the third way of implementation. It needs to implement the Serializable interface, which is implemented by byte stream. However, this way of implementation is that the third party provides ready-made methods, which we can call directly.

5. Deep cloning through JSON tool class

In this lesson, we use the JSON conversion tool Gson provided by Google to implement. Other JSON conversion tool classes are similar, and the implementation code is as follows:

import com.google.gson.Gson;

/**
 * Implementation mode 5 of deep clone: through JSON tool
 */
public class FifthExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // create object
        Address address = new Address(110, "Beijing");
        People p1 = new People(1, "Java", address);

        // Call Gson clone object
        Gson gson = new Gson();
        People p2 = gson.fromJson(gson.toJson(p1), People.class);

        // Modifying prototype objects
        p1.getAddress().setCity("Xi'an");

        // Output p1 and p2 address information
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * User class
     */
    static class People {
        private Integer id;
        private String name;
        private Address address;
        // Ignore constructor, set, get methods
    }

    /**
     * Address class
     */
    static class Address {
        private Integer id;
        private String city;
        // Ignore constructor, set, get methods
    }
}

The execution results of the above procedures are as follows:

p1: Xi'an p2: Beijing

Using the JSON tool class will first convert the object to a string, and then convert the new object from a string to a new object. Because the new object is converted from a string, it will not have any association with the prototype object, so deep cloning is realized. Other similar JSON tool classes are implemented in the same way.

Guess of clone design concept

The official didn't give a direct answer to why clones should be designed like this. We can only try to answer this question with some experience and source documents. There are two main steps to implement cloning in Java: one is to implement the clonable null interface, the other is to rewrite the Object's clone() method and then call the parent class's clone method( super.clone()), then why?

It can be seen from the source code that the clonable interface was born earlier, and JDK 1.0 already exists, so there has been a cloning method since then, so how can we identify that a class level object has a cloning method? Although cloning is important, we can't add it to every class by default, which is obviously not appropriate. Then we can only use these methods:

  • Add a new 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, for an important but not commonly used cloning function, It's obviously not appropriate to add a new class ID alone; besides, the second one, because the clone function appeared earlier, and there was no annotation function at that time, so it can't be used; the third one basically meets our needs, and the fourth one is similar to the first one, in order to sacrifice a base class for a clone function, and Java It can only be inherited, so this scheme is not suitable. With the exclusion method, there is no doubt that the way to implement the interface is the most reasonable solution at that time, and in the Java language, a class can implement multiple interfaces.

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

Because of the particularity of the semantics of the clone() method, it is better to have the direct support of the JVM. Since you want the direct support of the JVM, you need to find an API to expose this method. The most direct way is to put it into the base class Object of all classes, so that all classes can be called easily.

Summary

In this lesson, we talked about the concepts of shallow clone and deep clone, as well as the Convention of Object to clone(); we also demonstrated that the copyOf() method of array is actually shallow clone, and five implementation methods of deep clone; finally, we talked about the design idea of clone in Java language, hoping that these contents can help you practically.

Posted by northk on Fri, 19 Jun 2020 22:05:06 -0700