Java superinterfaces runtime difference Java 8 vs Java 9

Viewed 166

I noticed a difference in the output of the following program when run with Java 8 and Java 9.

import java.lang.reflect.Method;
public class OrderingTest {
    public static void main(String[] args) {
        ServiceImpl service = new ServiceImpl();
        for (Method method : service.getClass().getMethods()) {
            for (Class<?> anInterface : method.getDeclaringClass().getInterfaces()) {
                try {
                    Method intfMethod = anInterface.getMethod(method.getName(), method.getParameterTypes());
                    System.out.println("intfMethod = " + intfMethod);
                } catch (NoSuchMethodException e) { }
            }
        }
    }
}

class ServiceImpl implements ServiceX {
    @Override
    public Foo getType() { return null; }
}

interface ServiceX extends ServiceA<Foo>, ServiceB { }
abstract class Goo { }
class Foo extends Goo { }

interface ServiceA<S> {
    S getType();
}
interface ServiceB {
    @java.lang.Deprecated
    Goo getType();
}

You can run both versions of java here: https://www.jdoodle.com/online-java-compiler/

Java 8 outputs:

intfMethod = public abstract java.lang.Object ServiceA.getType()
intfMethod = public abstract java.lang.Object ServiceA.getType()
intfMethod = public abstract java.lang.Object ServiceA.getType()

Java 9 outputs:

intfMethod = public abstract Goo ServiceB.getType()
intfMethod = public abstract Goo ServiceB.getType()
intfMethod = public abstract Goo ServiceB.getType()

But when I reorder the super-interfaces to:

interface ServiceX extends ServiceB, ServiceA<Foo> { }

Then both versions of java output:

intfMethod = public abstract Goo ServiceB.getType()
intfMethod = public abstract Goo ServiceB.getType()
intfMethod = public abstract Goo ServiceB.getType()

I was wondering what is causing it? Is there a new java feature I am not aware of?

Java 8 documentation https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.8

Java 9 documentation https://docs.oracle.com/javase/specs/jls/se9/html/jls-8.html#jls-8.4.8

2 Answers

The difference seems to be in the implementation of getMethod API in use which is visible by the stated documentation starting Java-9 :

Within each such subset only the most specific methods are selected. Let method M be a method from a set of methods with same VM signature (return type, name, parameter types). M is most specific if there is no such method N != M from the same set, such that N is more specific than M. N is more specific than M if:

a. N is declared by a class and M is declared by an interface; or

b. N and M are both declared by classes or both by interfaces and N's declaring type is the same as or a subtype of M's declaring type (clearly, if M's and N's declaring types are the same type, then M and N are the same method).

While Java-8 follows up internally with interfaceCandidates.getFirst() (i.e. the order change matters here), the upgraded version seems to be working on the specific algorithm using res.getMostSpecific() before returning the method asked for.

Answer moved from body of question:


The root cause was that java type erasure at compile time creates a bridge method with a return type of "Object", which is less specific than a return type of "Goo". Java 9 was given an update to the implementation of Class.getMethod() to change from returning the first defined interface's implementation to instead return the most specific implementation.

foo@bar:~$ javap ./ServiceImpl.class
Compiled from "OrderingTest.java"
class ServiceImpl implements ServiceX {
  ServiceImpl();
  public Foo getType();
  public java.lang.Object getType(); <== here
  public Goo getType();
}

The old functionality from Java 8 can be maintained in Java 9 by adding another interface which overrides the generic method with an API making it more specific. This will work with Java 9 as it did in Java 8.

import java.lang.reflect.Method;
public class OrderingTest {
    public static void main(String[] args) {
        ServiceImpl service = new ServiceImpl();
        for (Method method : service.getClass().getMethods()) {
            for (Class<?> anInterface : method.getDeclaringClass().getInterfaces()) {
                try {
                    Method intfMethod = anInterface.getMethod(method.getName(), method.getParameterTypes());
                    System.out.println("intfMethod = " + intfMethod);
                } catch (NoSuchMethodException e) { }
            }
        }
    }
}

class ServiceImpl implements ServiceX {
    @Override
    public Foo getType() { return new Foo(); }
}

interface ServiceX extends ServiceAA, ServiceB { }
abstract class Goo { }
class Foo extends Goo { }
interface ServiceAA extends ServiceA<Foo> {
    @Override
    Foo getType();
}
interface ServiceA<S> {
    S getType();
}
interface ServiceB {
    @java.lang.Deprecated
    Goo getType();
}
Related