One advantage of the Java language is that it cancels the concept of pointers, but it also leads many programmers to often ignore the difference between objects and references in programming. This article will try to clarify this concept. And because Java cannot solve the problem of object copying through simple assignment, during the development process, it is often necessary to use the clone() method to copy objects. This article will let you understand what shadow clone and deep clone are, and understand their differences, advantages and disadvantages.
Are you a little confused when you see this title: the Java language clearly states that pointers are cancelled, because pointers are often convenient and also the root cause of code insecurity, and it also makes the program very complex and difficult to understand. The code written by abuse of pointers is no less than using the already infamous "GOTO" statement. It is absolutely wise for Java to give up the concept of pointers. But this is just that there is no clear pointer definition in the Java language. In essence, every new statement returns a pointer reference. However, in most cases, Java does not need to worry about how to operate this "pointer", let alone be as frightened as when operating pointers in C++. The only thing to care more about is when passing objects to functions.
package com.zoer.src; public class ObjRef { Obj aObj = new Obj(); int aInt = 11; public void changeObj(Obj inObj) { inObj.str = "changed value"; } public void changePri(int inInt) { inInt = 22; } public static void main(String[] args) { ObjRef oRef = new ObjRef(); System.out.println("Before call changeObj() method: " + oRef.aObj); oRef.changeObj(oRef.aObj); System.out.println("After call changeObj() method: " + oRef.aObj); System.out.println("=========================================================================================================================================================================================================================================================================================================================================================================================================================================================== call changePri() method: " + oRef.aInt); } } package com.zoer.src; public class Obj { String str = "init value"; public String toString() { return str; } } The main part of this code calls two very similar methods, changeObj() and changePri(). The only difference is that one takes the object as the input parameter, and the other takes the basic type int in Java as the input parameter. And the input parameters are changed inside both function bodies. The seemingly same method, but the output results of the program are different. The changeObj() method really changes the input parameters, while the changePri() method does not change the input parameters.
From this example, Java processes objects and basic data types differently. Like C, when Java's basic data types (such as int, char, double, etc.) are passed to the function body as entry parameters, the passed parameters become local variables inside the function body. This local variable is a copy of the input parameters. All operations inside the function body are operations for this copy. After the function execution is completed, the local variable will complete its mission and it will not affect the variables as input parameters. This method of parameter passing is called "value passing". In Java, the pass using an object as an entry parameter is "reference pass", which means that only one "reference" of the object is passed. The concept of this "reference" is the same as the pointer reference in C language. When the input variable is changed inside the function body, it is essentially a direct operation on the object.
Except for "reference pass" when passing the value of the function, "reference pass" when assigning values to object variables using "=". It's similar to giving a variable another alias. Both names point to the same object in memory.
In actual programming, we often encounter this situation: there is an object A that contains some valid values at a certain moment. At this time, a new object B may be needed exactly the same as A, and any changes to B will not affect the value in A. That is to say, A and B are two independent objects, but the initial value of B is determined by the A object. In Java language, using simple assignment statements cannot meet this requirement. Although there are many ways to meet this need, the simplest and most efficient way to implement the clone() method is among them.
All Java classes inherit the java.lang.Object class by default, and there is a method clone() in the java.lang.Object class. JDK API documentation explains this method will return a copy of the Object object. There are two points to be explained: First, the copy object returns a new object, not a reference. Second, the difference between copying an object and a new object returned with the new operator is that this copy already contains some information about the original object, rather than the initial information of the object.
How to apply the clone() method?
A very typical call to clone() code is as follows:
public class CloneClass implements Cloneable { public int aInt; public Object clone() { CloneClass o = null; try { o = (CloneClass) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return o; } } There are three points worth noting. First, the CloneClass class that hopes to implement the clone function implements the Cloneable interface. This interface belongs to the java.lang package. The java.lang package has been imported into the default class, so it does not need to be written as java.lang.Cloneable. Another thing worth noting is that the clone() method is overloaded. Finally, super.clone() is called in the clone() method, which also means that no matter what the inheritance structure of the clone class looks like, super.clone() directly or indirectly calls the clone() method of the java.lang.Object class. Let’s explain these points in detail below.
It should be said that the third point is the most important. If you carefully observe a native method of the Object class clone(), the efficiency of the native method is generally much higher than that of the non-native methods in Java. This also explains why we should use the clone() method in Object instead of first new a class and then assign the information from the original object to the new object, although this also implements the clone function. For the second point, we should also observe whether clone() in the Object class is a method with a protected property. This also means that if you want to apply the clone() method, you must inherit the Object class. In Java, all classes inherit the Object class by default, so you don't have to worry about this. Then overload the clone() method. Another thing to consider is that in order for other classes to call the clone() method of this clone class, after overloading, the attributes of the clone() method must be set to public.
So why does the clone class still implement the Cloneable interface? Note that the Cloneable interface does not contain any methods! In fact, this interface is just a flag, and this flag is only for the clone() method in the Object class. If the clone class does not implement the Cloneable interface and calls the Object's clone() method (that is, the super.Clone() method is called), then the Object's clone() method will throw a CloneNotSupportedException exception.
The above are the most basic steps of clone. If you want to complete a successful clone, you also need to understand what "shadow clone" and "deep clone" are.
What is shadow clone?
package com.zoer.src; class UnCloneA { private int i; public UnCloneA(int ii) { i = ii; } public void doublevalue() { i *= 2; } public String toString() { return Integer.toString(i); } } class CloneB implements Cloneable { public int aInt; public UnCloneA unCA = new UnCloneA(111); public Object clone() { CloneB o = null; try { o = (CloneB) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return o; } } public class ObjRef { public static void main(String[] a) { CloneB b1 = new CloneB(); b1.aInt = 11; System.out.println("before clone,b1.aInt = " + b1.aInt); System.out.println("before clone,b1.unCA = " + b1.unCA); CloneB b2 = (CloneB) b1.clone(); b2.aInt = 22; b2.unCA.doublevalue(); System.out.println("=============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== clone,b2.aInt = " + b2.aInt); System.out.println("after clone,b2.unCA = " + b2.unCA); } } Output result:
before clone,b1.aInt = 11before clone,b1.unCA = 111=================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================
The output results show that the int variable aInt and the clone result of the instance object unCA of UnCloneA are inconsistent. The int type is truly clone because the aInt variable in b2 has no effect on the aInt of b1. That is to say, b2.aInt and b1.aInt already occupy different memory spaces, b2.aInt is a real copy of b1.aInt. On the contrary, the change to b2.unCA changes simultaneously to b1.unCA, and it is obvious that b2.unCA and b1.unCA are different references that only point to the same object! From this we can see that the effect of calling the clone() method in the Object class is: first open a piece of space in memory that is the same as the original object, and then copy the contents in the original object as it is. For basic data types, such operations are not problematic, but for non-primitive type variables, we know that they only save references to objects, which also causes the non-primitive type variables after clone to point to the same object and the corresponding variables in the original object.
Most of the time, the results of this clone are often not the results we hope for, and this clone is also called "shadow clone". To make b2.unCA point to an object different from b2.unCA, and b2.unCA also contains information in b1.unCA as initial information, you must implement a depth clone.
How to perform deep clone?
It is very simple to change the above example to a deep clone, and two changes are required: one is to allow the UnCloneA class to also implement the same clone function as the CloneB class (implement the Cloneable interface and overload the clone() method). The second is to add a sentence o.unCA = (UnCloneA)unCA.clone() to the clone() method of CloneB;
package com.zoer.src; class UnCloneA implements Cloneable { private int i; public UnCloneA(int ii) { i = ii; } public void doublevalue() { i *= 2; } public String toString() { return Integer.toString(i); } public Object clone() { UnCloneA o = null; try { o = (UnCloneA) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return o; } } class CloneB implements Cloneable { public int aInt; public UnCloneA unCA = new UnCloneA(111); public Object clone() { CloneB o = null; try { o = (CloneB) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } o.unCA = (UnCloneA) unCA.clone(); return o; } } public class CloneMain { public static void main(String[] a) { CloneB b1 = new CloneB(); b1.aInt = 11; System.out.println("before clone,b1.aInt = " + b1.aInt); System.out.println("before clone,b1.unCA = " + b1.unCA); CloneB b2 = (CloneB) b1.clone(); b2.aInt = 22; b2.unCA.doublevalue(); System.out.println("============================================================================================================================================================================================================================================================================================================================================================================================= clone,b1.aInt = " + b1.aInt); System.out.println("after clone,b1.unCA = " + b1.unCA); System.out.println("======================================================================================================================================================================================================================================================================================================================================================================================================================================================================== Output result:
before clone,b1.aInt = 11before clone,b1.unCA = 111=================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================
It can be seen that the current change of b2.unCA has no effect on b1.unCA. At this time, b1.unCA and b2.unCA point to two different UnCloneA instances, and b1 and b2 have the same value at the moment when CloneB b2 = (CloneB)b1.clone(); is called, here, b1.i = b2.i = 11.
You should know that not all classes can implement deep clones. For example, if you change the UnCloneA type variable in the CloneB class above to the StringBuffer type, look at the description about StringBuffer in the JDK API. StringBuffer does not overload the clone() method, and what is more serious is that StringBuffer is still a final class, which means that we cannot indirectly implement the clone of StringBuffer using inheritance. If a class contains a StringBuffer type object or an object similar to StringBuffer, we have two choices: either only shadow clone can be implemented, or add a sentence to the clone() method of the class (assuming it is a SringBuffer object, and the variable name is still unCA): o.unCA = new StringBuffer(unCA.toString()); //The original one is: o.unCA = (UnCloneA)unCA.clone();
It should also be noted that in addition to the basic data types that can automatically implement deep clones, the String object is an exception. Its performance after clone seems to also implement deep clones. Although this is just an illusion, it greatly facilitates our programming.
The difference between String and StringBuffer in Clone should be explained that this is not a focus on the difference between String and StringBuffer, but from this example, we can also see some unique features of the String class.
The following example includes two classes. The CloneC class contains a String type variable and a StringBuffer type variable, and implements the clone() method. The CloneC type variable c1 is declared in the StrClone class, and then the clone() method of c1 is called to generate a copy of c1 c2. After changing the String and StringBuffer type variables in c2, the result is printed:
package com.zoer.src; class CloneC implements Cloneable { public String str; public StringBuffer strBuff; public Object clone() { CloneC o = null; try { o = (CloneC) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return o; } } public class StrClone { public static void main(String[] a) { CloneC c1 = new CloneC(); c1.str = new String("initializeStr"); c1.strBuff = new StringBuffer("initializeStrBuff"); System.out.println("before clone,c1.str = " + c1.str); System.out.println("before clone,c1.strBuff = " + c1.strBuff); CloneC c2 = (CloneC) c1.clone(); c2.str = c2.str.substring(0, 5); c2.strBuff = c2.strBuff.append(" change strBuff clone"); System.out.println("====================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== System.out.println("after clone,c2.strBuff = " + c2.strBuff); } } Execution results:
<span style="font-family:'Microsoft YaHei';"><span style="font-size:16px;">before clone,c1.str = initializeStr before clone,c1.strBuff = initializeStrBuff ======================================================================================================================================================================================================================================================================================================================================================================================================================================================== clone,c2.str = initializeStrBuff change strBuff clone </span></span>
The printed result shows that the variables of String type seem to have implemented depth clone, because the changes to c2.str have not affected c1.str! Does Java regard Sring class as a basic data type? Actually, this is not the case. There is a small trick here. The secret lies in the statement c2.str = c2.str.substring(0,5)! In essence, c1.str and c2.str are still references when clone, and both point to the same String object. But when c2.str = c2.str.substring(0,5), it is equivalent to generating a new String type and then assigning it back to c2.str. This is because String is written as an immutable class by Sun engineers, and functions in all String classes cannot change their own values.
The above is all about this article. I hope it will be helpful for everyone to understand deep and shallow replication in Java.