Обратите внимание, что компилятор работает не только с классами, такими как ваш текущий код, но он работает с полными типами, в зависимости от того, какой код вы пишете, он может повлиять на вас. Представьте себе:
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, чтобы проверить, можно ли назначать объекты небольшая оболочка для правильной обработки примитивных типов, как здесь.