DGoiko
DGoiko

Reputation: 378

Compiler-like Java Reflection method resolution

I'm programming a method that uses Reflection to find methods the "same way" (not exactly, as you will see) the compiler does. I left Generics out of the scope in this case, as the type parameter does not affect signature of methods (as far as I know).

Basically I want to ask how far are my methods from accomplishing the task in a relatively acceptable way (I don't expect perfect and full resolution, I don't even need static methods).

I could not find the exact spects about how does the compiler actually perform the task, so I made a guess that suits the needs of my environment, however, if possible and not too complex, I'd like to have it done in the proper way for possible complex future uses.

Right now, my assumption is IGNORING overloaded methods with different return types, as it is not a problem with my current problem, but that may be in the future, so I may be interested in taking it into consideration if you have ideas.

So, as I don't have to care about return types, I start by calling a simple

clazz.getMethods();

and then I perform a filter by name. I'm aware I'm totally missing the overrides mentioned abvove here. This is a first approach.

This is how I calculate the distance between a method and the desired arguments: If parent and children are the same instance, then the distance is 0. If not, this methods calls classDistance recursively on children superclass and all directly implemented interfaces. The distance will be the smalles positive distance plus one. (ignoring incompatible ancestors). This solution works for me because right now all the functions I need to call have just one parameter, and the few that have a second one, allways do a perfect match on first parameter on the desired method, so there's only one positive distance to narrow down.

Actual code:

private static Method getBestMatch(List<Method> validMethods, List<Class<?>> classes) {
    if(validMethods.size() == 1) return validMethods.get(0);
    int distance = Integer.MAX_VALUE;
    Method currMethod = null;
    outer_loop:
    for(Method method : validMethods) {
        Class<?>[] methodTypes = method.getParameterTypes();
        int methodDistance = 0;
        for(int i=0; i < methodTypes.length; i++) {
            if(!methodTypes[i].isAssignableFrom(classes.get(i))) continue outer_loop; // Incompatible. Should not happen, but just in case
            methodDistance += classDistance(methodTypes[i], classes.get(i));

        }
        if(currMethod == null || methodDistance < distance) {
            currMethod = method;
            distance = methodDistance;
        }
    }
    return currMethod;
}

Distance calculator:

private static int classDistance(Class<?> parent, Class<?> children) throws IllegalArgumentException{
    if(parent.equals(children)) return 0;
    if(!parent.isAssignableFrom(children)) throw new IllegalArgumentException("children is not assignable to father"); // Should do b4 equals?
    Integer minDistance = null;

    Class<?> superClass = children.getSuperclass();
    if(superClass != null && parent.isAssignableFrom(superClass)) {
        minDistance = classDistance(parent, superClass);
    }
    for(Class<?> directInterface : children.getInterfaces()) {
        if(!parent.isAssignableFrom(directInterface)) continue;
        int interfaceDistance = classDistance(parent, directInterface);
        if(interfaceDistance < minDistance)  minDistance = interfaceDistance;
    }

    if(minDistance == null) throw new IllegalArgumentException("we found no distance. this is an odd behaviour and definetly a bug, or means this method is not well-thought at all");
    return minDistance + 1;
}

Things I should take into consideration:

Upvotes: 1

Views: 306

Answers (2)

GotoFinal
GotoFinal

Reputation: 3685

Note that compiler does not work on just classes like your current code, but it works on full types, depending what kind of code you are writing it might affect you. Imagine this:

void someMethod(List<? extends Number> numbers);
void someMethod(Collection<String> numbers);

Then if you will look for method that can accept ArrayList you will find both, so if you don't know type of your data, you will not be able to provide valid answer. But generic type of variables is lost anyway, so if you are trying to find matching method for some list of arguments - you can't do that in good reliable way.

Object[] args = new Object[]{List.of("String")};
Class[] argTypes = extractTypes(args); // contains [SomeListImplementation.class]
findBestMatchingMethod(SomeClass.class, argTypes); // ?? both method matches

You could try to "rebuild" generic type by analyzing what is inside object, but that is very hard and would not always work (because raw types are used or object is empty). Would only be kinda possible for collection types to some limited degree.

But if this is enough, then java have some tools for this:

new Statement(object, "doSomething", new Object[]{arg1, arg2, arg3}).execute();

But return value is lost. But you can just copy code from jdk used to do this. I also once developed my own code to do something like that:

First we need some utils we will use later, like easy way to map between primitive and object types, as java compiler handles this for us, so we should too:

public final class ClassUtils {
    private static final Map<Class<?>, Class<?>> primitives = Map.of(
        Boolean.class,   boolean.class,     Byte.class,    byte.class,        Short.class, short.class,
        Character.class, char.class,        Integer.class, int.class,         Long.class,  long.class,
        Float.class,     float.class,       Double.class,  double.class,      Void.class,  void.class);
    private static final Map<Class<?>, Class<?>> wrappers   = Map.of(
        boolean.class, Boolean.class,       byte.class,   Byte.class,         short.class, Short.class,
        char.class,    Character.class,     int.class,    Integer.class,      long.class,  Long.class,
        float.class,   Float.class,         double.class, Double.class,       void.class,  Void.class);

    public static Class<?> getPrimitive(Class<?> clazz) {
        if (clazz.isPrimitive()) {
            return clazz;
        }
        return primitives.getOrDefault(clazz, clazz);
    }

    public static Class<?> getWrapperClass(Class<?> clazz) {
        if (! clazz.isPrimitive()) {
            return clazz;
        }
        return wrappers.getOrDefault(clazz, clazz);
    }
}

And simple util to check if object of typeA could be assigned to typeB but handling primitive types (as in java you can pass int to long, or int to Integer) too:

public final class TypeUtils {
    private TypeUtils() {}

    /**
     * Checks if given objectType can be assigned to variable of variableType type.
     *
     * @param objectType type of object that you want to assign.
     * @param variableType type of variable where object will be assigned.
     * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
     *
     * @return {@code true} if assignment possible
     */
    public static boolean isAssignable(@Nullable Class<?> objectType, final Class<?> variableType, final boolean autoboxing) {
        if (objectType == null) {
            return ! variableType.isPrimitive();
        }
        if (objectType == variableType) {
            return true;
        }

        if (autoboxing) {
            if (objectType.isPrimitive() && ! variableType.isPrimitive()) {
                objectType = ClassUtils.getWrapperClass(objectType);
            }
            if (variableType.isPrimitive() && ! objectType.isPrimitive()) {
                objectType = ClassUtils.getPrimitive(objectType);
                if (! objectType.isPrimitive()) {
                    return false;
                }
            }
        }

        if (objectType == variableType) {
            return true;
        }

        if (objectType.isPrimitive()) {
            if (! variableType.isPrimitive()) {
                return false;
            }
            if (Integer.TYPE.equals(objectType)) {
                return Long.TYPE.equals(variableType) || Float.TYPE.equals(variableType) || Double.TYPE.equals(variableType);
            }
            if (Long.TYPE.equals(objectType)) {
                return Float.TYPE.equals(variableType) || Double.TYPE.equals(variableType);
            }
            if (Boolean.TYPE.equals(objectType)) {
                return false;
            }
            if (Double.TYPE.equals(objectType)) {
                return false;
            }
            if (Float.TYPE.equals(objectType)) {
                return Double.TYPE.equals(variableType);
            }
            if (Character.TYPE.equals(objectType)) {
                return Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            if (Short.TYPE.equals(objectType)) {
                return Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            if (Byte.TYPE.equals(objectType)) {
                return Short.TYPE.equals(variableType)
                               || Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            return false;
        }
        return variableType.isAssignableFrom(objectType);
    }
}

Note: if you will be trying to make version that works with generics, apache commons have method to check if 2 generic types are assignable to each other, as I would not suggest writing it alone, its a lot of complicated code to handle all possible nested generic types.

And having these 2 simple utils we can start creating code to find this matching executable, first I made small enum to represent 3 possible states of method:

enum CompatibleExecutableResult {
    EXACT,
    COMPATIBLE,
    INVALID
}

It can either be perfect match or be compatible or no match, we have separate exact match because javac would then fail to compile due to ambiguous definitions, so we probably want to do the same.

So now a simple function that calculates that:

private static CompatibleExecutableResult isCompatibleExecutable(Method method, Class[] providedTypes) {
    Class<?>[] constructorParameterTypes = method.getParameterTypes();
    CompatibleExecutableResult current = CompatibleExecutableResult.EXACT;
    for (int i = 0; i < constructorParameterTypes.length; i++) {
        Class<?> providedType = providedTypes[i];
        Class<?> parameterType = constructorParameterTypes[i];

        // null can't be used as primitive
        if ((providedType == null) && parameterType.isPrimitive()) {
            return CompatibleExecutableResult.INVALID;
        }

        // handle primitives correctly by using our special util function
        if ((providedType != null) && !TypeUtils.isAssignable(parameterType, providedType, true)) {
            return CompatibleExecutableResult.INVALID;
        }

        // this code support skipping some types assuming that you will use null value for it, so you can look for any method with 3 arguments where you only know 2 first types and last one will be always null.
        if ((providedType == null)) {
            current = CompatibleExecutableResult.COMPATIBLE;
            continue;
        }

        if (parameterType.equals(providedType)) {
            continue; // sill exact match
        }
        // it was not an exact match as types of this argument were not equals, so thats our current max possible score for this method
        current = CompatibleExecutableResult.COMPATIBLE;
    }
    return current;
}

But now we could find few exact matches, or few compatible methods, so we also need a function that will tell us which method is more specialized:

static Method getMoreSpecialized(Method a, Method b) {
    if (a == null) return b;
    if (b == null) return a;
    Class<?>[] aTypes = a.getParameterTypes();
    Class<?>[] bTypes = b.getParameterTypes();
    int result = 0;
    for (int i = 0; i < aTypes.length; i++) {
        Class<?> aType = aTypes[i];
        Class<?> bType = bTypes[i];

        // same type, no differences so far
        if (aType.equals(bType)) {
            continue;
        }

        // if aType is less specialized than bType
        if ((aType.isPrimitive() && !bType.isPrimitive()) || TypeUtils.isAssignable(aType, bType, true)) {
            // one of prev types was less specialized, javac fails to find such constructor, we should too
            if (result < 0) {
                throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
            }
            result += 1;
        } else {
            if (result > 0) {
                throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
            }
            result -= 1;
        }
    }
    if (result == 0) {
        throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
    }
    return result < 0 ? a : b;
}

And then we can write our simple loop that will find best match, so loop over methods, check if compatible and check if more specialized than last one + be sure to not allow more than 1 exact match:

static <T> Method findBest(Collection<? extends Method> methods, Class[] paramTypes) {
    int exactMatches = 0;
    Method bestMatch = null;
    for (Method executable : methods) {
        CompatibleExecutableResult compatibleConstructor = isCompatibleExecutable(executable, paramTypes);
        if (compatibleConstructor == CompatibleExecutableResult.EXACT) {
            if (exactMatches >= 1) {
                throw new IllegalStateException("Ambiguous executables found " + Arrays.toString(paramTypes));
            }
            exactMatches += 1;
        }
        if (compatibleConstructor != CompatibleExecutableResult.INVALID) {
            bestMatch = getMoreSpecialized(bestMatch, executable);
        }
    }
    if (bestMatch == null) {
        throw new IllegalStateException("Can't find matching executable for: " + Arrays.toString(paramTypes));
    }
    return bestMatch;
}

Code supporting generic types should look similar, just use Type[] and that apache commons method to check if objects are assignable with a small wrapper to also handle primitive types correctly like here.

Upvotes: 0

Andreas
Andreas

Reputation: 159124

I could not find the exact spects about how does the compiler actually perform the task

They are in the Java Language Specification, section 15.12. Method Invocation Expressions, more precisely in subsection 15.12.2. Compile-Time Step 2: Determine Method Signature.

It's quite complex, and as you can see, far more involved than what you're currently trying to do.

I'll leave it up to you to read through all of sections 15.12.2 to 15.12.2.6, and then determine how much you can ignore and still "accomplishing the task in a relatively acceptable way".

Upvotes: 3

Related Questions