简体   繁体   中英

Reflection on interface overridden methods

I have the following code, with a generic ITest interface extended by a not generic ITestDouble interface. The op method is overridden by ITestDouble .

When I try to list all the methods of ITestDouble , I get op twice. How can I verify that they are actually the same method?

public class Test {

    public static void main(String[] args) throws NoSuchMethodException {
        for (Method m : ITestDouble.class.getMethods()) {
            System.out.println(m.getDeclaringClass() + ": " + m + "(bridge: " + m.isBridge() + ")");
        }
    }

    public interface ITestDouble extends ITest<Double> {
        @Override
        public int op(Double value);

        @Override
        public void other();
    }

    public interface ITest<T extends Number> {
        public int op(T value);

        public void other();
    }
}

Output:

interface Test$ITestDouble: public abstract int Test$ITestDouble.op(java.lang.Double)(bridge: false)
interface Test$ITestDouble: public abstract void Test$ITestDouble.other()(bridge: false)
interface Test$ITest: public abstract int Test$ITest.op(java.lang.Number)(bridge: false)

PS I know this is the same question as Java Class.getMethods() behavior on overridden methods , but that question got no real answer: the isBridge() call always returns false .

EDIT: I'm also fine with any library which would do the dirty job of filtering out the "duplicate" op method for me.

Unfortunately you cannot have that information, because as far as the JVM is concerned, ITestDouble has a legitimate method op(Number) which can be totally independent of op(Double) . It is actually your Java compiler that makes sure the methods always coincide.

That implies that you can create pathological implementations of ITestDouble with totally different implementations for op(Number) and op(Double) by using a pre-JDK5 compiler, or a dynamic proxy:

public static void main(String[] args) throws NoSuchMethodException {

    final Method opNumber = ITest.class.getMethod("op", Number.class);
    final Method opDouble = ITestDouble.class.getMethod("op", Double.class);
    final Method other = ITestDouble.class.getMethod("other");

    ITestDouble dynamic = (ITestDouble) Proxy.newProxyInstance(
            ITestDouble.class.getClassLoader(),
            new Class<?>[]{ITestDouble.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
                    if (opDouble.equals(m)) return 1;
                    if (opNumber.equals(m)) return 2;
                    // etc....

                    return null;
                }
            });

    System.out.println("op(Double): " + dynamic.op(null);            // prints 1.
    System.out.println("op(Number): " + ((ITest) dynamic).op(null);  // prints 2. Compiler gives warning for raw types
}

EDIT: Just learned of Java ClassMate . It is a library that can correctly resolve all type variables in a declaration. It is very easy to use:

    TypeResolver typeResolver = new TypeResolver();
    MemberResolver memberResolver = new MemberResolver(typeResolver);

    ResolvedType type = typeResolver.resolve(ITestDouble.class);
    ResolvedTypeWithMembers members = memberResolver.resolve(type, null, null);
    ResolvedMethod[] methods = members.getMemberMethods();

Now if you iterate over methods you'll see the following:

void other();
int op(java.lang.Double);
int op(java.lang.Double);

Now it is easy to filter for duplicates:

public boolean canOverride(ResolvedMethod m1, ResolvedMethod m2) {
    if (!m1.getName().equals(m2.getName())) return false;

    int count = m1.getArgumentCount();
    if (count != m2.getArgumentCount()) return false;

    for (int i = 0; i < count; i++) {
        if (!m1.getArgumentType(i).equals(m2.getArgumentType(i))) return false;
    }

    return true;
}

Every respondent so far has positively contributed, but I'll try to wrap the different ideas up into a single answer. The key to understanding what is going on is how the Java compiler and JVM implement generics - that explains the bridge, and why it is false in the interface.

In summary, though:

  1. The more generic method int op(Number) is required for signature compatibility and to construct the implementation's bridge method

  2. The isBridge() method can only be true for concrete classes, not interfaces

  3. It probably doesn't matter which of the two methods you pick up - they will work identically in the runtime.

Ok, here's the long answer:

Java's implementation of Generics

When you have a generic class or interface, the compiler constructs a method in the class file with each generic type replaced with an appropriate concrete type. For example, ITest has a method:

int op(T value)

where the class defines T as T extends Number , so the class file has a method:

int op(Number);    

When ITest is used, the compiler creates additional classes for each type the generic is resolved to. For example, if there is a line of code:

ITest<Double> t = new ...

The compiler produces a class with the following methods:

int op(Number);
int op(Double);

But surely the JVM only needs the int op(Double) version? Doesn't the compiler make sure that ITest<Double> only receives calls on op(Double) ?

There are two reasons why Java needs the int op(Number) method:

  1. Object orientation requires that wherever you use a class you can always replace it by a subclass (at least from type safety). If int op(Number) did not exist, the class will not provide a full implementation of the signature of the superclass (or super-interface).

  2. Java is a dynamic language with both casting and reflection, so it is possible to call the method with an incorrect type. At this point Java guarantees that you get a class cast exception.

In fact, the implementation of 2. above is achieved by the compiler producing a 'bridge method'.

What does a bridge method do?

When ITest<Double> is created from ITest<T extends Number> , the compiler creates the int op(Number) method, and its implementation is:

public int op(Number n) {
    return this.op((Double) n);
} 

This implementation has two properties:

  1. Where n is a Double , it delegates the call to int op(Double) , and

  2. Where n is not a Double , it causes the ClassCastException .

This method is a 'bridge' from the generic type to the concrete type. From its very nature, only concrete methods can be bridges , so the int op(Double) on the sub-interface is only a signature.

What about the OP?

In the example in the question, the sub-interface class file ITestDouble created by the compiler has both the methods:

int op(Number);
int op(Double);

The int op(Number) is needed so that implementations of ITestDouble can have their bridge method - but this method is not itself a bridge because it is only a signature, not an implementation. Arguably Sun/Oracle have missed a trick here, and it might be worth raising a bug with them.

How to find the correct method?

First, does it matter? All implementations of ITestDouble will have the bridge method inserted automatically by the compiler and the bridge method calls the int op(Double) method. In other words, it really doesn't matter which method is called, just pick one.

Second, at runtime you most likely will be passed instances , not Interfaces. When you do the getMethods() , you will be able to distinguish between the bridge method and the actual implementation. This is what johncarl said.

Third, if you do need to solve this by interrogating the interface, you might want to test the arguments for the 'lowest' subtype. Eg, in a meta level:

  1. Collect all the two methods with the same name

  2. Collect the argument type: Method.getParameterTypes()[0]

  3. Use Class.isAssignableFrom(Class) . The method returns true if the argument is the same or a subclass of the class on which the method is called.

  4. Use the method with the argument being the subclass of the other method's argument.

This might work for what you need. Tweak it for specific needs or any fail cases. In short, it checks to see if the method matches the exact 'generic' type declared for the class from which is declared. If you are using your own generics, this might fall-over. I would suggest combining calls to getDeclaringClass() with the logic for your own generics.

public static boolean matchesGenericSignature(Method m) {
    Type[] parameters = m.getGenericParameterTypes();
    if (parameters.length == 0)
        return false;
    Class<?> declaring = m.getDeclaringClass();
    TypeVariable<?>[] types = declaring.getTypeParameters();
    for (TypeVariable<?> typeVariable : types) {
        for (Type parameter : parameters) {
            if (typeVariable.equals(parameter)) {
                return true;
            }
        }
    }
    return false;
}

Update:

For your solution try this method (Method.getGenericParameterTypes()):

public static void main(String[] args) {

    for (Method m : ITestDouble.class.getMethods()) {
        Type [] types = m.getGenericParameterTypes();
        System.out.println(m.getDeclaringClass() + ": " + m + "(genericParameterTypes: "
                + Arrays.toString(types) + ")"+" "+(types.length>0?types[0].getClass():""));

        Type t = types.length>0?types[0]:null;
        if(t instanceof TypeVariable){
            TypeVariable<?> v = (TypeVariable)t;
            System.out.println(v.getName()+": "+Arrays.toString(v.getBounds()));
        }
    }

}

Output is:

interface FakeTest$ITestDouble: public abstract int FakeTest$ITestDouble.op(java.lang.Double)(genericParameterTypes: [class java.lang.Double]) class java.lang.Class
interface FakeTest$ITestDouble: public abstract void FakeTest$ITestDouble.other()(genericParameterTypes: []) 
interface FakeTest$ITest: public abstract int FakeTest$ITest.op(java.lang.Number)(genericParameterTypes: [T]) class sun.reflect.generics.reflectiveObjects.TypeVariableImpl
T: [class java.lang.Number]

Generics are erased during compilation. So you really have:

   public interface ITestDouble extends ITest {

        public int op(Double value);

        @Override
        public void other();
    }

    public interface ITest {
        public int op(Number value);

        public void other();
    }

Class ITest do not know how many implementation you have. So it has only one method op with parameter Number. You can define infinite implementation with T extends Number . (in your T = Double).

You can use the method getDeclaredMethods() instead of the getMethods() to get only the methods declared on the class you are reflecting (not super classes). It solves the problem of having duplicated methods.

I know this may not answer your question or solve your problem 100%, but you can use the isBridge() method to determine what methods are implemented by a concrete class vs what methods are generically 'bridged' as such:

public class Test {

    public static void main(String[] args) throws NoSuchMethodException {
        for (Method m : TestDouble.class.getMethods()) {
            System.out.println(m.getDeclaringClass() + ": " + m + "(bridge: " + m.isBridge() + ")");
        }
    }

    public class TestDouble extends ITestDouble{
        public int op(Double value) {
            return 0;
        }

        public void other() {
        }
    }

    public interface ITestDouble extends ITest<Double> {

        public int op(Double value);

        public void other();
    }

    public interface ITest<T extends Number> {
        public int op(T value);

        public void other();
    }
}

outputs:

class test.Test$TestDouble: public int test.Test$TestDouble.op(java.lang.Double)(bridge: false)
class test.Test$TestDouble: public int test.Test$TestDouble.op(java.lang.Number)(bridge: true)
class test.Test$TestDouble: public void test.Test$TestDouble.other()(bridge: false)
class java.lang.Object: public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException(bridge: false)
class java.lang.Object: public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException(bridge: false)
class java.lang.Object: public final void java.lang.Object.wait() throws java.lang.InterruptedException(bridge: false)
class java.lang.Object: public boolean java.lang.Object.equals(java.lang.Object)(bridge: false)
class java.lang.Object: public java.lang.String java.lang.Object.toString()(bridge: false)
class java.lang.Object: public native int java.lang.Object.hashCode()(bridge: false)
class java.lang.Object: public final native java.lang.Class java.lang.Object.getClass()(bridge: false)
class java.lang.Object: public final native void java.lang.Object.notify()(bridge: false)
class java.lang.Object: public final native void java.lang.Object.notifyAll()(bridge: false)

Notably:

Test$TestDouble.op(java.lang.Number)(bridge: true)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM