3

Assume we have the following interface and implementations:

interface Matcher<T>{
     boolean matches(T arg);
}

class NumberMatcher<T extends Number> implements Matcher<T>{
    @Override
    public boolean matches(T arg){...}
}

class StringMatcher extends Matcher<String>{
   @Override 
   public boolean matches(String arg){ ...}
}

class CustomMatcher extends NumberMatcher<Integer> {
    public boolean matches(String arg){...}

    @Override
    public boolean matches(Integer arg){...}
}

What I need is the type of the parameter of the matches(T) method of a given Matcher implementation.

NumberMatcher numberMatcher = new NumberMatcher<Long>();
StringMatcher stringMatcher = new StringMatcher();
CustomMatcher customMatcher = new CustomMatcher();
Matcher<Date> dateMatcher = new Matcher<Date>(){...};

getArgumentType(numberMatcher) // should return Number.class
getArgumentType(stringMatcher) // should return String.class
getArgumentType(customMatcher) // should return Integer.class
getArgumentType(dateMatcher ) // should return Object.class

Here is a implementation that works except of the CustomMatcher case, cause it fails to detect the overriden matches(..) method and returns String.class instead of Integer.class.

Class<?> getArgumentType(Matcher<?> matcher) {
    Method[] methods = matcher.getClass().getMethods();
    for (Method method : methods) {
        if (isMatchesMethod(method)) {
            return method.getParameterTypes()[0];
        }

    }
    throw new NoSuchMethodError("Method 'matches(T)' not found!");
}

private boolean isMatchesMethod(Method method) {
    if (!isPublic(method.getModifiers()))
        return false;

    if (method.getParameterCount() != 1)
        return false;

    return method.getName().equals("matches");
}

EDIT:

I am looking for a solution that doesn't need to specify the argument type like this:

interface Matcher<T>{
     boolean matches(T arg);
     Class<T> argumentType();
}
Chriss
  • 5,157
  • 7
  • 41
  • 75
  • Just for reference: An obvious solution would be to apply `getDeclaredAnnotations()` to the method. But this will not work in this case because the `@Override` annotation is no longer accessible at runtime. – Frank Puffer Jun 28 '16 at 07:20
  • Maybe [`Method.getDeclaringClass`](https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html#getDeclaringClass--) will help? – Seelenvirtuose Jun 28 '16 at 07:23
  • @Seelenvirtuose Method.getDeclaringClass() doesn't work in case `CustomMatcher` cause the declaring class is `NumberMatcher` not the interface `Matcher` – Chriss Jun 28 '16 at 07:32

1 Answers1

0

As long as you can edit the implementations, you can use a marker annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ThisIsTheOne {

}

public class CustomMatcher extends NumberMatcher<Integer> {

    @Override
    @ThisIsTheOne
    public boolean matches(Integer arg){ return true; }

    public boolean matches(String arg){  return true; }
}

private static boolean isMatchesMethod(Method method) {
    if (method.getAnnotation(ThisIsTheOne.class) != null) {
        return true;
    }
    // do the same as before, so it works on non-annotated methods too
}

This will return Integer.class for CustomMatcher.

I don't think there's a way to retrieve this information at runtime, since Method-s don't know where they come from. This is probably intentional, as multiple interfaces can define the same method signature.

Tamas Rev
  • 7,008
  • 5
  • 32
  • 49