A DESCRIPTION OF THE PROBLEM :
JVMS §5.4.3.3 Method Resolution says:
>[When] method resolution attempts to locate the referenced method in the superinterfaces of the specified class C:
>
>If the maximally-specific superinterface methods of C for the name and descriptor specified by the method reference include exactly one method that does not have its ACC_ABSTRACT flag set, then this method is chosen and method lookup succeeds.
>
>Otherwise, if any superinterface of C declares a method with the name and descriptor specified by the method reference that has neither its ACC_PRIVATE flag nor its ACC_STATIC flag set, one of these is arbitrarily chosen and method lookup succeeds."
I.e., if we have a class which inherits from its interfaces two equally-specific competing definitions of a default method, one of these is chosen arbitrarily.
In reality, as shown by the attached test case, the VM throws an error:
java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.foo I2.foo
In trying to determine if it is the VM or the spec that is wrong here, I notice that JLS §13.5.7, which gives an equivalent example involving "Cowboy" and "CowboyArtist", says that an IncompatibleClassChangeError will be thrown.
---------- BEGIN SOURCE ----------
import java.lang.classfile.ClassFile;
import java.lang.constant.*;
import java.lang.reflect.Method;
class MaxSpecificBug {
public static void main(String... args) throws Throwable {
// public class C implements I1, I2 {}
byte[] cBytes = ClassFile.of().build(ClassDesc.of("C"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_SUPER);
cb.withInterfaceSymbols(ClassDesc.of("I1"), ClassDesc.of("I2"));
cb.withMethodBody("<init>", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.aload(0);
cdb.invokespecial(ConstantDescs.CD_Object, "<init>", ConstantDescs.MTD_void);
cdb.return_();
});
});
// public interface I1 { default void foo() { System.out.println("Hello from I1"); } }
byte[] i1Bytes = ClassFile.of().build(ClassDesc.of("I1"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_INTERFACE | ClassFile.ACC_ABSTRACT);
cb.withMethodBody("foo", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"));
cdb.ldc("Hello from I1");
cdb.invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object));
cdb.return_();
});
});
// public interface I2 { default void foo() { System.out.println("Hello from I2"); } }
byte[] i2Bytes = ClassFile.of().build(ClassDesc.of("I2"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_INTERFACE | ClassFile.ACC_ABSTRACT);
cb.withMethodBody("foo", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"));
cdb.ldc("Hello from I2");
cdb.invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object));
cdb.return_();
});
});
DirectClassLoader dcl = new DirectClassLoader();
dcl.load(i1Bytes);
dcl.load(i2Bytes);
Class<?> clss = dcl.load(cBytes);
Object instance = clss.getConstructor().newInstance();
Method foo = clss.getMethod("foo");
// expected: "Hello from I1" or "Hello from I2"; no error
// reality: IncompatibleClassChangeError
foo.invoke(instance);
}
static final class DirectClassLoader extends ClassLoader {
public Class<?> load(byte[] classBytes) {
return super.defineClass(null, classBytes, 0, classBytes.length);
}
}
}
---------- END SOURCE ----------
JVMS §5.4.3.3 Method Resolution says:
>[When] method resolution attempts to locate the referenced method in the superinterfaces of the specified class C:
>
>If the maximally-specific superinterface methods of C for the name and descriptor specified by the method reference include exactly one method that does not have its ACC_ABSTRACT flag set, then this method is chosen and method lookup succeeds.
>
>Otherwise, if any superinterface of C declares a method with the name and descriptor specified by the method reference that has neither its ACC_PRIVATE flag nor its ACC_STATIC flag set, one of these is arbitrarily chosen and method lookup succeeds."
I.e., if we have a class which inherits from its interfaces two equally-specific competing definitions of a default method, one of these is chosen arbitrarily.
In reality, as shown by the attached test case, the VM throws an error:
java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.foo I2.foo
In trying to determine if it is the VM or the spec that is wrong here, I notice that JLS §13.5.7, which gives an equivalent example involving "Cowboy" and "CowboyArtist", says that an IncompatibleClassChangeError will be thrown.
---------- BEGIN SOURCE ----------
import java.lang.classfile.ClassFile;
import java.lang.constant.*;
import java.lang.reflect.Method;
class MaxSpecificBug {
public static void main(String... args) throws Throwable {
// public class C implements I1, I2 {}
byte[] cBytes = ClassFile.of().build(ClassDesc.of("C"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_SUPER);
cb.withInterfaceSymbols(ClassDesc.of("I1"), ClassDesc.of("I2"));
cb.withMethodBody("<init>", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.aload(0);
cdb.invokespecial(ConstantDescs.CD_Object, "<init>", ConstantDescs.MTD_void);
cdb.return_();
});
});
// public interface I1 { default void foo() { System.out.println("Hello from I1"); } }
byte[] i1Bytes = ClassFile.of().build(ClassDesc.of("I1"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_INTERFACE | ClassFile.ACC_ABSTRACT);
cb.withMethodBody("foo", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"));
cdb.ldc("Hello from I1");
cdb.invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object));
cdb.return_();
});
});
// public interface I2 { default void foo() { System.out.println("Hello from I2"); } }
byte[] i2Bytes = ClassFile.of().build(ClassDesc.of("I2"), cb -> {
cb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_INTERFACE | ClassFile.ACC_ABSTRACT);
cb.withMethodBody("foo", ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, cdb -> {
cdb.getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"));
cdb.ldc("Hello from I2");
cdb.invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object));
cdb.return_();
});
});
DirectClassLoader dcl = new DirectClassLoader();
dcl.load(i1Bytes);
dcl.load(i2Bytes);
Class<?> clss = dcl.load(cBytes);
Object instance = clss.getConstructor().newInstance();
Method foo = clss.getMethod("foo");
// expected: "Hello from I1" or "Hello from I2"; no error
// reality: IncompatibleClassChangeError
foo.invoke(instance);
}
static final class DirectClassLoader extends ClassLoader {
public Class<?> load(byte[] classBytes) {
return super.defineClass(null, classBytes, 0, classBytes.length);
}
}
}
---------- END SOURCE ----------