Подобный компилятору Java разрешение метода отражения - PullRequest
1 голос
/ 29 февраля 2020

Я программирую метод, который использует Reflection, чтобы найти методы "таким же образом" (не совсем, как вы увидите), как это делает компилятор. В этом случае я исключил Generics из области видимости, поскольку параметр типа не влияет на сигнатуру методов (насколько я знаю).

В основном я хочу спросить, как далеко мои методы от выполнения задание относительно приемлемым способом (я не ожидаю идеального и полного разрешения, мне даже не нужны методы stati c).

Я не смог найти точные указания о том, как компилятор фактически выполняет задачу, поэтому я предположил, что соответствует потребностям моего окружения , однако, если это возможно и не слишком сложно, я бы хотел, чтобы это было сделано надлежащим образом для возможных сложных будущие использования.

Прямо сейчас, я предполагаю, что IGNORING перегружены методы с различными типами возвращаемых данных, так как это не проблема для моей текущей проблемы, но это может произойти в будущем, поэтому я могу быть заинтересован в ее принятии. во внимание, если у вас есть идеи.

Итак, поскольку мне не нужно заботиться о типах возвращаемых данных, я начинаю с вызова простого

clazz.getMethods();

, а затем я выполнить фильтр по имени. Я знаю, что я полностью пропускаю переопределения, упомянутые выше здесь. Это первый подход.

Вот как я вычисляю расстояние между методом и желаемыми аргументами: если родительский и дочерний элементы - это один и тот же экземпляр, тогда расстояние равно 0. Если нет, этот метод вызывает classDistance рекурсивно на детей суперкласс и все напрямую реализованные интерфейсы. Расстояние будет наименьшим положительным расстоянием плюс один. (игнорируя несовместимых предков). Это решение работает для меня, потому что сейчас все функции, которые мне нужно вызывать, имеют только один параметр, и те немногие, у которых есть второй, всегда идеально соответствуют первому параметру в нужном методе, поэтому есть только одно положительное расстояние для сужения вниз.

Фактический код:

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;
}

Калькулятор расстояния:

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;
}

Что следует учитывать:

  • Должен ли я пожаловаться на неоднозначные методы? Как вы видите, я просто взял первый
  • , который перегружен проблемой другого типа ... Как я могу знать приоритет? Если я не ошибаюсь, компилятор всегда выбирает метод ближайшего класса.
  • Этот расчет расстояния выглядит слишком простым, чтобы быть правдой

Ответы [ 2 ]

3 голосов
/ 29 февраля 2020

Я не смог найти точных указаний о том, как компилятор на самом деле выполняет задачу

Они находятся в Java Спецификации языка , секция 15.12. Выражения вызова метода , точнее в подразделе 15.12.2. Шаг 2: Определение сигнатуры метода .

Это довольно сложно и, как вы можете видеть, гораздо сложнее, чем то, что вы сейчас пытаетесь сделать.

I ' Предоставлю вам возможность прочитать все разделы с 15.12.2 по 15.12.2.6, а затем определить, сколько вы можете игнорировать и все же «выполнить задачу относительно приемлемым способом» .

0 голосов
/ 01 марта 2020

Обратите внимание, что компилятор работает не только с классами, такими как ваш текущий код, но он работает с полными типами, в зависимости от того, какой код вы пишете, он может повлиять на вас. Представьте себе:

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

Тогда, если вы будете искать метод, который может принять ArrayList, вы найдете оба, поэтому, если вы не знаете тип ваших данных, вы не сможете предоставить действительные ответ. Но переменные типа generi c все равно теряются, поэтому, если вы пытаетесь найти подходящий метод для некоторого списка аргументов - вы не можете сделать это надежным способом.

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

Вы можете попробовать «перестроить» тип generi c путем анализа того, что находится внутри объекта, но это очень сложно и не всегда работает (потому что используются необработанные типы или объект пуст). Будет возможно только для коллекционных типов в некоторой ограниченной степени.

Но если этого достаточно, то у java есть несколько инструментов для этого:

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

Но возвращаемое значение теряется. Но вы можете просто скопировать код из jdk, используемый для этого. Я также однажды разработал свой собственный код, чтобы сделать что-то подобное:

Сначала нам понадобятся некоторые утилиты, которые мы будем использовать позже, например, простой способ отображения между типами примитивов и объектами, поскольку компилятор java обрабатывает это для нас поэтому мы тоже должны:

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);
    }
}

И простой утилита для проверки, может ли объект типа A быть назначен типу B, но обрабатывать примитивные типы (как в java, вы можете передать int в long или От int до Integer) тоже:

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);
    }
}

Примечание: если вы будете пытаться создать версию, которая работает с дженериками, у apache commons есть метод проверки, если 2 типа generi c присваиваемые друг другу, так как я бы не советовал писать их в одиночку, это сложный код для обработки всех возможных вложенных обобщенных типов c.

И имея эти 2 простых утилиты, мы можем начать создавать код, чтобы найти этот соответствующий исполняемый файл, сначала я сделал небольшое перечисление, чтобы представить 3 возможных состояния метода:

enum CompatibleExecutableResult {
    EXACT,
    COMPATIBLE,
    INVALID
}

Он может быть идеальным соответствовать или быть совместимыми или не соответствовать, у нас есть отдельное точное соответствие, потому что javac не сможет компилироваться из-за неоднозначных определений, поэтому мы, вероятно, хотим сделать то же самое.

Итак, теперь простая функция, которая вычисляет это:

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;
}

Но теперь мы можем найти несколько точных совпадений или несколько совместимых методов, поэтому нам также нужна функция, которая сообщит нам, какой метод более специализирован:

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;
}

И тогда мы можем написать наш простой l oop, который найдет наилучшее совпадение, поэтому l oop over методы, проверить совместимость и проверить, более ли специализирован, чем последний + убедитесь, что не разрешено более одного точного совпадения:

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;
}

Код, поддерживающий типы generi c, должен выглядеть одинаково, просто используйте Type[] и метод apache commons, чтобы проверить, можно ли назначать объекты небольшая оболочка для правильной обработки примитивных типов, как здесь.

...