Thursday, October 14, 2010

Issue of autoboxing and reflection

Problem: Consider that you want to have a method called "Object invokeAndGet(Object targetObject, String methodName, Object... args)". This method should be able to invoke the given method on the target object and return the value returned by the invocation. If the method has overloaded forms, then depending on the argument types, the invokeAndGet method should invoke the correct form.

Solution:
Though this problem looks trivial and seems like it can be solved by using reflections APIs, it gets really tricky when you have to deal with primitive types. This is due to the fact that autoboxing converts the primitives into their corresponding wrapper types. For e.g. an integer value is autoboxed into Integer object, etc.

To begin with let us assume that we have the following implementation of the method:

    public static Object invokeAndGet(Object obj, String methodName, Object... args) {
        try {
            Class<?>[] argsTypes = new Class[args.length];
            for(int i = 0; i < argsTypes.length; i++) {
                argsTypes[i] = args[i].getClass();
            }
            Method m = obj.getClass().getMethod(methodName, argsTypes);
            Object retObj = m.invoke(obj, args);
            return retObj;
        } catch (Exception e) {
            System.out.println("**** Exception thrown: " + e);
            return null;
        }
    }

What this code is straight forward. It gets the types of the arguments. Attempts to find out if the target object's class has any method matching the method name and the argument types. If one is found, then that method is invoked. Otherwise null is returned.

The tricky part is this. Assume that the target object's class has a method "void setX(int x)". If you attempt to do invokeAndGet(targetObject, "setX", 1), it will fail. The reason is because of the fact that the argument 1 is converted to its wrapper type, which is Integer. So when you look up, you are looking up for the method with the signature "setX(Integer)", where as the method that is present in the target object has the signature "setX(int)".

There is no bullet proof solution for this problem. One of the ways you can try to solve this issue is by retrying with primitive types when you get a NoSuchMethodException. The modified version of the code is given below. It works for most of the cases:
    public static Object invokeAndGet(Object obj, String methodName, Object... args) {
        try {
            Class<?>[] argsTypes = new Class[args.length];
            for(int i = 0; i < argsTypes.length; i++) {
                argsTypes[i] = args[i].getClass();
            }
            Method m = null;
            try {
                m = obj.getClass().getMethod(methodName, argsTypes);
            } catch (NoSuchMethodException nsme) {
                boolean signatureModified = false;
                for(int i = 0; i < argsTypes.length; i++) {
                    Field field;
                    try {
                        field = argsTypes[i].getField("TYPE");
                        if(Modifier.isStatic(field.getModifiers())) {
                            argsTypes[i] = (Class<?>) field.get(null);
                            signatureModified = true;
                        }
                    } catch (Exception e) {
                        // Ignore these exceptions. There is nothing we can do by catching them.
                    }
                }
                if(signatureModified)
                    m = obj.getClass().getMethod(methodName, argsTypes);
            }
            Object retObj = null;
            if(m != null)
                retObj = m.invoke(obj, args);
            return retObj;
        } catch (Exception e) {
            System.out.println("*** Exception thrown: " + e);
            return null;
        }
    }    


What I am doing is very simple. Whenever NoSuchMethodException is thrown, I convert all the wrapper classes to their respective primitive classes by accessing the "TYPE" static field in them. For e.g. Integer.TYPE will give me int.class. Then I retry to get the new method, if I the signature had been modified since the last attempt.

I said that almost works. Here are the corner cases:
1) If your method has mixed type arguments, some of them are primitives and some of them are wrappers, the code given above will not work. For e.g. if you have a method with signature "setXIf(int oldValue, Integer newValue)", then invokeAndGet(targetObject, "setXIf", 1, 2) will fail.
2) There is a remote case when one of the argument class has a field called TYPE which is of type Class. In such cases also the above code will fail.

Please let me know if you know of a better method than the one given above to reflectively invoke a method on an object. I will appreciate that.

No comments: