views:

119

answers:

4

As part of developing a small ScriptEngine, I reflectively call java methods. A call by the script engine gives me the object the method name and an array of arguments. To call the method I tried to resolve it with a call to Class.getMethod(name, argument types).
This however only works when the classes of the arguments and the classes expected by the Method are the same.

Object o1 = new Object();
Object out = System.out;
//Works as System.out.println(Object) is defined
Method ms = out.getClass().getMethod("println",o1.getClass());
Object o2 = new Integer(4);
//Does not work as System.out.println(Integer) is not defined
Method mo = out.getClass().getMethod("println",o2.getClass());

I would like to know if there is a "simple" way to get the right method, if possible with the closest fit for the argument types, or if I have to implement this myself.

Closest fit would be:

Object o1 = new Integer(1);
Object o2 = new String("");
getMethod(name, o1.getClass())//println(Object)
getMethod(name, o2.getClass())//println(String)  

Update:
To clarify what I need: The Script Engine is a small project I write in my free time so there are no strikt rules I have to follow. So I thought that selecting methods called from the Engine the same way the java compiler selects methods at compile time only with the dynamic type and not the static type of the Object would work.(with or without autoboxing)
This is what I first hoped that the Class.getMethod() would solve. But the Class.getMethod() requires the exact same Classes as argument types as the Method declares, using a subclass will result in a no such method Exception. This may happen for good reasons, but makes the method useless for me, as I don't know in advance which argument types would fit.
An alternate would be to call Class.getMethods() and iterate through the returned array and try to find a fitting method. This would however be complicated if I don't just want to take the first "good" method which I come across, so I hoped that there would be an existing solution which at least handles:

  • closest fit: If arg.getClass() == subclass and methods m(Superclass), m(Subclass) then call m(Subclass)
  • variable arguments: System.out.printf(String ,String...)

Support for autoboxing would be nice, too.
If a call cannot be resolved it may throw an exception ( ma(String,Object), ma(Object, String), args= String,String)
(If you made it till here, thanks for taking the time to read it:-))

+1  A: 

AFAIK, there is no simple way to do this kind of thing. Certainly, there's nothing in the standard Java class libraries to do this.

The problem is that there is no single "right" answer. You need to consider all of your use-cases, decide what the "right method" should be and implement your reflection code accordingly.

Stephen C
+1  A: 

The javadoc for Class.getMethod() states that

To find a matching method in a class C: If C declares exactly one public method with the specified name and exactly the same formal parameter types, that is the method reflected. If more than one such method is found in C, and one of these methods has a return type that is more specific than any of the others, that method is reflected; otherwise one of the methods is chosen arbitrarily.

So if you're just concerned about the type, it's already handled. But if you are concerned with the number of arguments instead, then you'll have to implement it yourself.

aberrant80
@aberrant80 - '... it's already handled'. I don't think that "one of the methods is chosen arbitrarily" counts as adequate handling. The method selection rules need to be deterministic and transparent.
Stephen C
@Stephen C: IMHO it _is_ adequate handling, because sometimes you can't define anything better. Consider a class that has 2 methods: `public void foo(String a, Object b)` and `public void foo(Object a, String b)`. Now a client calls `foo("x", "x")`. How do you propose to select a method deterministically?
Eli Acherkan
@Eli - a simple rule is to throw an exception in all ambiguous cases. Assuming that we correctly codify what is ambiguous, this will be *both* well-defined *and* deterministic. By contrast, the advertised behavior of `Class.getMethod()` is *neither*.
Stephen C
@aberrant80 it only handles the return type right, but it doesn't handle subclasses in the argument array, a method accepting Object as argument wont be found with Integer.class
josefx
+2  A: 

I would suggest that you use getMethods(). It returns an array of all public methods (Method[]).

The most important thing here is:
"If the class declares multiple public member methods with the same parameter types, they are all included in the returned array."

What you will then need to do is to use the results in this array to determine which one of them (if any) are the closest match. Since what the closest match should be depends very much on your requirements and specific application, it does make sense to code it yourself.


Sample code illustrating one approach of how you might go about doing this:

public Method getMethod(String methodName, Class<?> clasz)
{
    try
    {
        Method[] methods = clasz.getMethods();
        for (Method method : methods)
        {
            if (methodName.equals(method.getName()))
            {
                Class<?>[] params = method.getParameterTypes();
                if (params.length == 1)
                {
                    Class<?> param = params[0];
                    if ((param == int.class) || (param == float.class) || (param == float.class))
                    {
                        //method.invoke(object, value);
                        return method;
                    }
                    else if (param.isAssignableFrom(Number.class))
                    {
                        return method;
                    }
                    //else if (...)
                    //{
                    //    ...
                    //}
                }
            }
        }
    }
    catch (Exception e)
    {
        //some handling
    }
    return null;
}

In this example, the getMethod(String, Class<?>) method will return a method that with only one parameter which is an int, float, double, or a superclass of Number.

This is a rudimentary implementation - It returns the first method that fits the bill. You would need to extend it to create a list of all methods that match, and then sort them according to some sort of criteria, and return the best matching method.

You can then take it even further by creating the more general getMethod(String, Class<?>) method, to handle more of the possible "close match" scenarios, and possibly even more than one paramter

HTH


Edit: As @finnw has pointed out, be careful when using Class#isAssignableFrom(Class<?> cls), due to its limitations, as I have in my sample code, testing the primitives separately from the Number objects.

bguiz
@bguiz, nice solution. Just want to add that you can test for `Class.isPrimitive()` and `Class.isArray()` to work around some of the limitations of isAssignableFrom(). @finnw's answer too.
bojangle
+2  A: 

As others have pointed out there is no standard method that does this, so you are going to have to implement your own overload resolution algorithm.

It would probably make sense to follow javac's overload resolution rules as closely as possible:
http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#292575
You can probably ignore generics for a dynamically-typed scripting language, but you might still benefit from the bridge methods that the compiler generates automatically.

Some pitfalls to watch out for:

  • Class.isAssignableFrom does not know about automatic widening primitive conversions, because these are syntactic sugar implemented in the compiler; They do not occur in the VM or class hierarchy. e.g. int.class.isAssignableFrom(short.class) returns false.
  • Similarly Class.isAssignableFrom does not know about auto-boxing. Integer.class.isAssignableFrom(int.class) returns false.
  • Class.isInstance and Class.cast take an Object as an argument; You cannot pass primitive values to them. They also return an Object, so they cannot be used for unboxing ((int) new Integer(42) is legal in Java source but int.class.cast(new Integer(42)) throws an exception.)
finnw