Forum Stats

  • 3,750,522 Users
  • 2,250,187 Discussions
  • 7,866,997 Comments

Discussions

The Implementation Of Perfect Clone.

LitnhJacuzzi
LitnhJacuzzi Member Posts: 2 Green Ribbon
edited Jul 24, 2021 8:44AM in Java Programming

Clone is an important technology in Java, but we can only implement a clone operation by overriding clone() method or serializations. And now, we can achieve a PERFECT clone by my method which cost me a huge amount of time to research.

Implementation details:

Pre-Analysis

You might have thought of directly copying serialization method and removing the exception thrown caused by not implementing the Serializable interface. But if you have thought further, you would find a fatal problem that serialization has no way to deal with the fields modified transient. Also, you might have coded a native method to implement it by copy the memory, but that's not safe. Therefore, the reflection method is preferred.

Here is the basic logical structure:

public static <T> T clone(T target) {
    //Judge the type of the target, such as null and objects that can be cloned directly.
    if(target == null)
	return null;
		
    if(canDireclyClone(target.getClass()))
	return target;
		
    //Judge the Object type to avoid some exceptions below.
    if(target.getClass() == Object.class)
	return (T) new Object();

    T ret = null;

    //Array is a very peculiar type, we should handle it specifically.
    if(target.getClass().isArray()) {
        //Operations.
    }

    ret = instantiateObject(target.getClass()); //We discuss this method later.

    ArrayList<Field> fields = new ArrayList<Field>();
    //Get all fields.
    do {
	fields.addAll(Arrays.asList(iterator.getDeclaredFields()));
    }while((iterator = iterator.getSuperclass()) != Object.class);

    //Clone every field.
    for(Field field : fields) {
        field.set(...); //Callback the clone method.
    }

    return ret;
}

This outline seems perfect, and if we fill in the content, it will look like this:

private static <T> T clone(T target) {
	if(target == null)
		return null;
	
	if(canDireclyClone(target.getClass()))
		return target;
	
	if(target.getClass() == Object.class)
		return (T) new Object();
	
        T ret = null;

	if(target.getClass().isArray()) {
		if(ret != null) {
			if(ret.getClass() != target.getClass())
				ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
		}else {
			ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
		}
		
		if(Array.getLength(target) == 0) return (T) ret;
			
		for(int i = 0; i < Array.getLength(target); i++) {
			Array.set(ret, i, clone(Array.get(target, i), Array.get(ret, i)));
		}
		
		return (T) ret;
	}
	
	ret = (T) instantiateObject(target.getClass());

	ArrayList<Field> fields = new ArrayList<Field>();
	Class<?> iterator = target.getClass();
	do {
		fields.addAll(Arrays.asList(iterator.getDeclaredFields()));
	}while((iterator = iterator.getSuperclass()) != Object.class);

	for(Field field : fields) {
		try {
                        //Skip static fields.
			if(!(Modifier.isStatic(field.getModifiers()))) {
				field.setAccessible(true);
				Object fieldTargetValue = field.get(target);
				field.set(ret, clone(fieldTargetValue, field.get(ret)));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	return (T) ret;
}      

Exceptions Handling

So, can the above code successfully achieve a perfect clone? In fact, it is far from enough. Now, let's discuss the difficult exception handling below.

  • StackOverflowError & IllegalArguementException:

The first exception I met was StackOverflowError. Why did this happen? Through a detailed analysis of the exception throwing information, I finally understood that this is a symbiosis reference problem (as I call it). In short, the fields in the two classes refer to each other's instances. Let's use an image to illustrate this situation vividly:









According to this image, when we clone the b field in class A, the method will try to clone the object of class B, and there is a field of type A in class B, the method will try to clone an object of class A, when the method is executed again to clone the b field in class A, the above-mentioned endless loop will be formed. To solve this problem, only let the method know that the object of type A has been cloned and directly reference the object of type A that has been cloned(you can think deeply about the principle) . And to achieve that, we were SUPPOSEDE to use the HashMap to store the cloned objects(actually the HashMap is not reliable, we will learn the reason later). The modified code as follows:

private static <T> T clone(T target, Object ret) {
	if(target == null)
		return null;
	
	if(canDireclyClone(target.getClass()))
		return target;
	
	if(target.getClass() == Object.class)
		return (T) new Object();

        int index = clonedObjects.indexOf(target);
        if(index != -1) return (T) clonedRetObjects.get(index);
	
        T ret = null;

	if(target.getClass().isArray()) {
		if(ret != null) {
			if(ret.getClass() != target.getClass())
				ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
		}else {
			ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
		}

	        clonedObjects.add(target);
	        clonedRetObjects.add(ret);
		
		if(Array.getLength(target) == 0) return (T) ret;
			
		for(int i = 0; i < Array.getLength(target); i++) {
			Array.set(ret, i, clone(Array.get(target, i), Array.get(ret, i)));
		}
		
		return (T) ret;
	}
	
	ret = (T) instantiateObject(target.getClass());
	
        //The type of these two fields is IArrayList(overrided), they replace the function of                   HashMap. 
	clonedObjects.add(target);
	clonedRetObjects.add(ret);
	
	ArrayList<Field> fields = new ArrayList<Field>();
	Class<?> iterator = target.getClass();
	do {
		fields.addAll(Arrays.asList(iterator.getDeclaredFields()));
	}while((iterator = iterator.getSuperclass()) != Object.class);


	for(Field field : fields) {
		try {
			if(!(Modifier.isStatic(field.getModifiers()))) {
				field.setAccessible(true);
				fieldTargetValue = field.get(target);
				field.set(ret, clone(fieldTargetValue, field.get(ret)));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	return (T) ret;
}

As you can see, I used IArrayList(I will explain it in the end) instead of HashMap. As for why, here is just a brief explanation: similar hash code generation algorithms in some types will cause the index calculation of HashMap to overlap and produce wrong mapping relationships, which will cause an IllegalArguementExcpetion thrown by field.set(...) method.

  • InaccessibleObjectException

This exception only appears after JDK 9 which introduces the module mechanism. It is caused by operating fields of classes that are not open to the current module. I used a gentle method to deal with it, which makes those classes open to our module. I wrote a hackPackage() method for it:

private static void hackPackage(Class<?> targetClass) {
	try {
		Method addOpens = Module.class.getDeclaredMethod("implAddExportsOrOpens", 
				String.class, Module.class, boolean.class, boolean.class);
		addOpens.setAccessible(true);
		addOpens.invoke(targetClass.getModule(), targetClass.getPackageName(), PerfectClone.class.getModule(), true, true);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

Ultimate Version

Now, let's reveal the final implementation:

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;

import sun.misc.Unsafe;

public class PerfectClone 
{
	private static IArrayList clonedObjects = new IArrayList();
	private static IArrayList clonedRetObjects = new IArrayList();
	private static Object fieldTargetValue = null;
	private static Unsafe unsafe = hackUnsafe();
	
	public static <T> T clone(T target) {
		clonedObjects.clear();
		clonedRetObjects.clear();
		return clone0(target, null);
	}
	
	/**
	 * Add {@code ret} parameter to reduce workload.
	 */
	@SuppressWarnings("unchecked")
	private static <T> T clone0(T target, Object ret) {
		if(target == null)
			return null;
		
		if(canDirectlyClone(target.getClass()))
			return target;
		
		if(target.getClass() == Object.class)
			return (T) new Object();
		
		int index = clonedObjects.indexOf(target);
		if(index != -1) return (T) clonedRetObjects.get(index);
		
		if(target.getClass().isArray()) {
			if(ret != null) {
				if(ret.getClass() != target.getClass())
					ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
			}else {
				ret = (T) Array.newInstance(target.getClass().getComponentType(), Array.getLength(target));
			}
			
			if(Array.getLength(target) == 0) return (T) ret;
			
			clonedObjects.add(target);
			clonedRetObjects.add(ret);
				
			for(int i = 0; i < Array.getLength(target); i++) {
				Array.set(ret, i, clone0(Array.get(target, i), Array.get(ret, i)));
			}
			
			return (T) ret;
		}
		
		if(ret != null) {
			if(ret.getClass() != target.getClass()) {
				ret = (T) instantiateObject(target.getClass());
			}
		}else {
			ret = (T) instantiateObject(target.getClass());
		}
		
		clonedObjects.add(target);
		clonedRetObjects.add(ret);
		
		ArrayList<Field> fields = new ArrayList<Field>();
		Class<?> iterator = target.getClass();
		hackPackage(iterator);
		do {
			hackPackage(iterator);
			fields.addAll(Arrays.asList(iterator.getDeclaredFields()));
		}while((iterator = iterator.getSuperclass()) != Object.class);

		for(Field field : fields) {
			try {
				if(!(Modifier.isStatic(field.getModifiers()))) {
					field.setAccessible(true);
 					fieldTargetValue = field.get(target);
					field.set(ret, clone0(fieldTargetValue, field.get(ret)));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		return (T) ret;
	}
	
	/**
	 * Use magical {@code Unsafe} to instantiate an object.
	 */
	@SuppressWarnings("unchecked")
	private static <T> T instantiateObject(Class<T> type) {
		try {
			return (T) unsafe.allocateInstance(type);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private static Unsafe hackUnsafe() {
		try {
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			return (Unsafe) field.get(null);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private static void hackPackage(Class<?> targetClass) {
		try {
			Method addOpens = Module.class.getDeclaredMethod("implAddExportsOrOpens", 
					String.class, Module.class, boolean.class, boolean.class);
			addOpens.setAccessible(true);
			addOpens.invoke(targetClass.getModule(), targetClass.getPackageName(), PerfectClone.class.getModule(), true, true);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static boolean canDirectlyClone(Class<?> targetClass) {
		if(targetClass.isPrimitive() || (targetClass == String.class) || 
				(targetClass == Class.class) || (targetClass == Module.class) ||
				(targetClass.isEnum()) || (targetClass == Character.class) ||
				(targetClass == Boolean.class) || (targetClass == Byte.class) ||
				(targetClass == Integer.class) || (targetClass == Long.class) ||
				(targetClass == Float.class) || (targetClass == Double.class))
			return true;
		return false;
	}
	
	/**
	 * I overrode the {@code indexOf(Object)} method because this method in ArrayList is not proper(the {@code equals()} method should not be used here).
	 */
	private static class IArrayList extends ArrayList<Object>
	{
		/**
		 * 
		 */
		private static final long serialVersionUID = 962042845135067366L;


		@Override
		public int indexOf(Object o) {
			for(int i = 0 ; i < size(); i++) {
				if(o == get(i)) {
					return i;
				}
			}
			return -1;
		}
	}
}

Attention: If your JDK version is 8 or lower, please remove the hackPackage() method with its references and the Module.class judgment in the canDirectlyClone() method.

You can directly use this.

Extra Notes

You need to pay attention to the occasion when using this method, because this method will clone an object that is exactly the same in the true sense, and this may not be suitable for our needs in some cases. For example, our cloning operation needs to avoid some fields, and rewriting the clone method may be a better choice in this case. Furthermore, some perfect clones of objects will have problems, such as JComponent, which will throw exceptions, because the cloned object has not been verified (refer to its source code for details).