0

I am trying to create a utility method that should be able to deep-clone any object. (Object.clone() only works on Object implementing Cloneable and I heard it's flawed anyways.)

I am using Objenesis to create new instances of objects without the use of constructors.

However, when trying to clone a JFrame I get the following Exception:
(using this class because I think it should be a good and complex test)

java.lang.InstantiationError: [Ljava.util.concurrent.ConcurrentHashMap$Node;
    at sun.reflect.GeneratedSerializationConstructorAccessor12.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:48)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)

I am open to any solution, not necessarily limited to Objenesis.

My Code:

private static ObjenesisStd OBJENESIS = new ObjenesisStd();

@SuppressWarnings("unchecked")
public static <T> T clone(T object, boolean deep){
    if(object == null){
        return null;
    }else{
        try {
            T clone = (T) OBJENESIS.newInstance(object.getClass());
            List<Field> fields = ReflectionUtil.getAllFieldsInHierarchy(object.getClass());
            for(Field field : fields){
                boolean isAccessible = field.isAccessible();
                boolean isFinal = ReflectionUtil.isFinal(field);
                field.setAccessible(true);
                ReflectionUtil.setFinal(field, false);
                Class<?> type = field.getType();
                if(!deep || type.isPrimitive() || type == String.class){
                    field.set(clone, field.get(object));
                }else{
                    field.set(clone, clone(field.get(object), true));
                }
                field.setAccessible(isAccessible);
                ReflectionUtil.setFinal(field, isFinal);
            }
            return clone;
        } catch (Throwable e) {
            e.printStackTrace();
            //throw new RuntimeException("Failed to clone object of type " + object.getClass(), e);
            return null;
        }
    }
}


public static void main(String[] args) {
    GetterSetterAccess access = new GetterSetterAccess(JFrame.class);
    JFrame frame = new JFrame("Test Frame");
    for(String attr : access.getAttributes()){
        System.out.println(attr + " " + access.getValue(frame, attr));
    }

    System.out.println("----------------------------------------------");
    frame = clone(frame, true);


    for(String attr : access.getAttributes()){
        System.out.println(attr + " " + access.getValue(frame, attr));
    }
}

EDIT: Got it to work with the accepted answer and a few more fixes:

  • Avoided cloning Wrappers of Primitive Types (Integer.class etc.)
  • Avoided cloning Classes (Objects of the class Class.class)
  • Stored the cloned objects in a Map and reused them, so if Object A has a reference to Object B and Object B one to Object A it doesn't get stuck in an infinite loop. I also used a Map that checks for exact equality (==) instead of using equals().
  • Created a custom exception class which would just be passed on instead of throwing a new exception on every level (causing a huge caused-by-depth).
Lahzey
  • 373
  • 5
  • 17
  • It should work perfectly. Which Objenesis version are you using and which JVM and version? – Henri Sep 17 '19 at 00:22
  • Objenesis 3.0.1 and JDK 1.8.0_212 – Lahzey Sep 17 '19 at 09:33
  • Changed it to Objenesis 2.5 and JDK 1.6.0_25 (because I'd prefer it to be compatible with Java 6) and it still does not work. The release notes of Objenesis 2.6 read 'Drop Java 5 support', so I assume Java 6 should work with the previous version. – Lahzey Sep 17 '19 at 10:11
  • It's pretty bad to clone complicated objects like that, also you are not skipping static fields. You will have a lot of issues with cloning objects like that, I could understand solution like that for simple beans/dto classes. – GotoFinal Sep 17 '19 at 14:41
  • Yeah I know and I don't really plan on cloning Swing Objects as that would probably break Swing. I just used JFrame because I knew that's about as complicated as Objects can get and I want it to just work for everything. However I do need a clone method that is capable of cloning Maps, which is what's failing here. Static fields are already removed by ReflectionUtil.getAllFieldsInHierarchy (my own utility). – Lahzey Sep 17 '19 at 14:48
  • Ah, also remember that you will have issues when running such code on java 9+ as you might start cloning internal java classes and cause errors for that. Would be good to add special cases to handle maps and collections, by creating new one of same type and adding all values to it. – GotoFinal Sep 18 '19 at 11:02

1 Answers1

2

I finally figured it out. Your code doesn't handle arrays. So it fails with instantiating "[Ljava.util.concurrent.ConcurrentHashMap$Node;" which is an array of Nodes.

However, I will advocate that indeed, you should not do that. You will end up with fairly complicated code. Depending on what you want to do, you could use Jackson or XStream to do a marshall / unmarshall to perform the copy.

If you really want to continue that path, you will need something like this after the null check of your clone method.

    if(object.getClass().isArray()) {
        int length = Array.getLength(object);
        Object array = Array.newInstance(object.getClass().getComponentType(), length);
        for (int i = 0; i < length; i++) {
            Array.set(array, i, clone(Array.get(object, i), true));
        }
        return (T) array;
    }
GotoFinal
  • 3,585
  • 2
  • 18
  • 33
Henri
  • 5,551
  • 1
  • 22
  • 29