java - Encontrar el método sobrecargado más específico usando MethodHandle
dynamic reflection (5)
Supongamos que tengo tres métodos dentro de un tipo determinado (clase / interfaz):
public void foo(Integer integer);
public void foo(Number number);
public void foo(Object object);
Usando un MethodHandle
o reflexión, me gustaría encontrar el método sobrecargado más específico para un objeto cuyo tipo solo se conoce en tiempo de ejecución. es decir, me gustaría hacer JLS 15.12 en tiempo de ejecución.
Por ejemplo, supongamos que tengo lo siguiente en un método del tipo mencionado anteriormente que contiene esos tres métodos:
Object object = getLong(); // runtime type is Long *just an example*
MethodHandles.lookup()
.bind(this, "foo", methodType(Void.class, object.getClass()))
.invoke(object);
Entonces, conceptualmente, querría que se elija foo(Number number)
, pero lo anterior arrojará una excepción ya que la API solo buscará un método foo(Long)
y nada más. Tenga en cuenta que el uso de Long
aquí es solo un ejemplo. El tipo de objeto podría ser cualquier cosa en la práctica; String, MyBar, Integer, ..., etc., etc.
¿Hay algo en la API de MethodHandle que automáticamente y en tiempo de ejecución tenga el mismo tipo de resolución que el compilador después de JLS 15.12?
Básicamente busqué todos los métodos que se pueden ejecutar con un conjunto de parámetros. Entonces, los ordené por la distancia entre parameterType y methodParameterType. Al hacer esto, podría obtener el método sobrecargado más específico.
Probar:
@Test
public void test() throws Throwable {
Object object = 1;
Foo foo = new Foo();
MethodExecutor.execute(foo, "foo", Void.class, object);
}
El Foo:
class Foo {
public void foo(Integer integer) {
System.out.println("integer");
}
public void foo(Number number) {
System.out.println("number");
}
public void foo(Object object) {
System.out.println("object");
}
}
El MethodExecutor:
public class MethodExecutor{
private static final Map<Class<?>, Class<?>> equivalentTypeMap = new HashMap<>(18);
static{
equivalentTypeMap.put(boolean.class, Boolean.class);
equivalentTypeMap.put(byte.class, Byte.class);
equivalentTypeMap.put(char.class, Character.class);
equivalentTypeMap.put(float.class, Float.class);
equivalentTypeMap.put(int.class, Integer.class);
equivalentTypeMap.put(long.class, Long.class);
equivalentTypeMap.put(short.class, Short.class);
equivalentTypeMap.put(double.class, Double.class);
equivalentTypeMap.put(void.class, Void.class);
equivalentTypeMap.put(Boolean.class, boolean.class);
equivalentTypeMap.put(Byte.class, byte.class);
equivalentTypeMap.put(Character.class, char.class);
equivalentTypeMap.put(Float.class, float.class);
equivalentTypeMap.put(Integer.class, int.class);
equivalentTypeMap.put(Long.class, long.class);
equivalentTypeMap.put(Short.class, short.class);
equivalentTypeMap.put(Double.class, double.class);
equivalentTypeMap.put(Void.class, void.class);
}
public static <T> T execute(Object instance, String methodName, Class<T> returnType, Object ...parameters) throws InvocationTargetException, IllegalAccessException {
List<Method> compatiblesMethods = getCompatiblesMethods(instance, methodName, returnType, parameters);
Method mostSpecificOverloaded = getMostSpecificOverLoaded(compatiblesMethods, parameters);
//noinspection unchecked
return (T) mostSpecificOverloaded.invoke(instance, parameters);
}
private static List<Method> getCompatiblesMethods(Object instance, String methodName, Class<?> returnType, Object[] parameters) {
Class<?> clazz = instance.getClass();
Method[] methods = clazz.getMethods();
List<Method> compatiblesMethods = new ArrayList<>();
outerFor:
for(Method method : methods){
if(!method.getName().equals(methodName)){
continue;
}
Class<?> methodReturnType = method.getReturnType();
if(!canBeCast(returnType, methodReturnType)){
continue;
}
Class<?>[] methodParametersType = method.getParameterTypes();
if(methodParametersType.length != parameters.length){
continue;
}
for(int i = 0; i < methodParametersType.length; i++){
if(!canBeCast(parameters[i].getClass(), methodParametersType[i])){
continue outerFor;
}
}
compatiblesMethods.add(method);
}
if(compatiblesMethods.size() == 0){
throw new IllegalArgumentException("Cannot find method.");
}
return compatiblesMethods;
}
private static Method getMostSpecificOverLoaded(List<Method> compatiblesMethods, Object[] parameters) {
Method mostSpecificOverloaded = compatiblesMethods.get(0);
int lastMethodScore = calculateMethodScore(mostSpecificOverloaded, parameters);
for(int i = 1; i < compatiblesMethods.size(); i++){
Method method = compatiblesMethods.get(i);
int currentMethodScore = calculateMethodScore(method, parameters);
if(lastMethodScore > currentMethodScore){
mostSpecificOverloaded = method;
lastMethodScore = currentMethodScore;
}
}
return mostSpecificOverloaded;
}
private static int calculateMethodScore(Method method, Object... parameters){
int score = 0;
Class<?>[] methodParametersType = method.getParameterTypes();
for(int i = 0; i < parameters.length; i++){
Class<?> methodParameterType = methodParametersType[i];
if(methodParameterType.isPrimitive()){
methodParameterType = getEquivalentType(methodParameterType);
}
Class<?> parameterType = parameters[i].getClass();
score += distanceBetweenClasses(parameterType, methodParameterType);
}
return score;
}
private static int distanceBetweenClasses(Class<?> clazz, Class<?> superClazz){
return distanceFromObjectClass(clazz) - distanceFromObjectClass(superClazz);
}
private static int distanceFromObjectClass(Class<?> clazz){
int distance = 0;
while(!clazz.equals(Object.class)){
distance++;
clazz = clazz.getSuperclass();
}
return distance;
}
private static boolean canBeCast(Class<?> fromClass, Class<?> toClass) {
if (canBeRawCast(fromClass, toClass)) {
return true;
}
Class<?> equivalentFromClass = getEquivalentType(fromClass);
return equivalentFromClass != null && canBeRawCast(equivalentFromClass, toClass);
}
private static boolean canBeRawCast(Class<?> fromClass, Class<?> toClass) {
return fromClass.equals(toClass) || !toClass.isPrimitive() && toClass.isAssignableFrom(fromClass);
}
private static Class<?> getEquivalentType(Class<?> type){
return equivalentTypeMap.get(type);
}
}
Por supuesto, se puede mejorar con algunas refactorizaciones y comentarios.
Dadas las limitaciones que: a) el tipo del parámetro solo se conoce en el tiempo de ejecución, yb) solo hay un parámetro, una solución simple puede ser simplemente subir la jerarquía de clases y escanear las interfaces implementadas como en el siguiente ejemplo.
public class FindBestMethodMatch {
public Method bestMatch(Object obj) throws SecurityException, NoSuchMethodException {
Class<?> superClss = obj.getClass();
// First look for an exact match or a match in a superclass
while(!superClss.getName().equals("java.lang.Object")) {
try {
return getClass().getMethod("foo", superClss);
} catch (NoSuchMethodException e) {
superClss = superClss.getSuperclass();
}
}
// Next look for a match in an implemented interface
for (Class<?> intrface : obj.getClass().getInterfaces()) {
try {
return getClass().getMethod("foo", intrface);
} catch (NoSuchMethodException e) { }
}
// Last pick the method receiving Object as parameter if exists
try {
return getClass().getMethod("foo", Object.class);
} catch (NoSuchMethodException e) { }
throw new NoSuchMethodException("Method not found");
}
// Candidate methods
public void foo(Map<String,String> map) { System.out.println("executed Map"); }
public void foo(Integer integer) { System.out.println("executed Integer"); }
public void foo(BigDecimal number) { System.out.println("executed BigDecimal"); }
public void foo(Number number) { System.out.println("executed Number"); }
public void foo(Object object) { System.out.println("executed Object"); }
// Test if it works
public static void main(String[] args) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
FindBestMethodMatch t = new FindBestMethodMatch();
Object param = new Long(0);
Method m = t.bestMatch(param);
System.out.println("matched " + m.getParameterTypes()[0].getName());
m.invoke(t, param);
param = new HashMap<String,String>();
m = t.bestMatch(param);
m.invoke(t, param);
System.out.println("matched " + m.getParameterTypes()[0].getName());
}
}
No pude encontrar una manera de hacer esto con MethodHandle
s, pero hay un java.beans.Statement
interesante que implementa la búsqueda del método más específico de JLS según los Javadocs :
El método de
execute
encuentra un método cuyo nombre es el mismo que la propiedadmethodName
e invoca el método en el destino. Cuando la clase del objetivo define muchos métodos con el nombre dado, la implementación debe elegir el método más específico utilizando el algoritmo especificado en la Especificación del lenguaje Java (15.11).
Para recuperar el Method
sí, podemos hacerlo utilizando la reflexión. Aquí hay un ejemplo de trabajo:
import java.beans.Statement;
import java.lang.reflect.Method;
public class ExecuteMostSpecificExample {
public static void main(String[] args) throws Exception {
ExecuteMostSpecificExample e = new ExecuteMostSpecificExample();
e.process();
}
public void process() throws Exception {
Object object = getLong();
Statement s = new Statement(this, "foo", new Object[] { object });
Method findMethod = s.getClass().getDeclaredMethod("getMethod", Class.class,
String.class, Class[].class);
findMethod.setAccessible(true);
Method mostSpecificMethod = (Method) findMethod.invoke(null, this.getClass(),
"foo", new Class[] { object.getClass() });
mostSpecificMethod.invoke(this, object);
}
private Object getLong() {
return new Long(3L);
}
public void foo(Integer integer) {
System.out.println("Integer");
}
public void foo(Number number) {
System.out.println("Number");
}
public void foo(Object object) {
System.out.println("Object");
}
}
No, no he visto nada parecido en MethodHandle API. Algo similar existe en commons-beanutils
como MethodUtils#getMatchingAccessibleMethod
por lo que no tiene que implementar eso.
Se verá algo como esto:
Object object = getLong();
Method method = MethodUtils.getMatchingAccessibleMethod(this.getClass(), "foo", object.getClass());
Puede convertir a MethodHandle API o simplemente usar el Method
directamente:
MethodHandle handle = MethodHandles.lookup().unreflect(method);
handle.invoke(this, object);
Puede usar MethodFinder.findMethod()
para lograrlo.
@Test
public void test() throws Exception {
Foo foo = new Foo();
Object object = 3L;
Method method = MethodFinder.findMethod(Foo.class, "foo", object.getClass());
method.invoke(foo, object);
}
public static class Foo {
public void foo(Integer integer) {
System.out.println("integer");
}
public void foo(Number number) {
System.out.println("number");
}
public void foo(Object object) {
System.out.println("object");
}
}
Como está en la biblioteca raíz java, sigue a JLS 15.12.