The JVM is required to ensure that, in certain circumstances, types mentioned in classes handled be different loaders mean the same thing (JVMS 5.3.4). For methods, there are two kinds of constraints:
- Given a Methodref or InterfaceMethodref appearing in a class file C: at resolution time, where the resolved method m1 is declared in class file D, the types mentioned in m1's descriptor must mean the same thing in C's and D's class loaders (5.4.3.3, 5.4.3.4).
- Given a method declaration m1 appearing in a class file C: at preparation time, where m1 overrides a method m2 in a class file D, the types mentioned by m1's descriptor must mean the same thing in C's and D's class loaders (5.4.2).
Because the VM generates bridges ("overpasses") in a class that is distinct from C or D, the timing and meaning of class loading constraints is different than if the VM were to directly implement default methods (with adjustments to method selection, etc.)
---
Example:
Let L1, L2, and L3 be class loaders with possibly-different interpretations of the type "Foo".
Loaded by L1:
public interface I { public default Foo m() { return null; } }
Loaded by L2:
public class A implements I {}
Loaded by L3:
public class Test { static {
new A()
invokevirtual A.m()Foo
} }
Expected (specified) behavior: 'I' and 'A' can be loaded cleanly (no constraints). In 'Test', 'A.m' resolves to 'I.m'. Loading constraint upon resolution of the invocation: Foo[L3]=Foo[L1].
Actual behavior: 'A.m' contains a bridge that redeclares 'm()Foo'. Loading constraint on preparation of A: Foo[L2]=Foo[L1]. In 'Test', 'A.m' resolves to 'A.m'. Loading constraint upon resolution of the invocation: Foo[L3]=Foo[L2].
What can go wrong?
- If Foo[L3]=Foo[L1], but Foo[L1/L3]!=Foo[L2], there should be no error, but one will be reported.
- If Foo[L2]!=Foo[L1], an error can occur upon preparation of A, even though no error should occur until the invocation is resolved.
The attached sources can be used to generate various tests that illustrate this second point. Instructions and outcomes (in each case, uncomment the noted items, compile/run, then restore the comments before the next test):
declare A.m: success
declare A.m, declare B.m: overriding error (B vs. A)
declare A.m, invoke A.m: resolution error (Task vs. A)
declare A.m, invoke B.m: resolution error (Task vs. A)
declare I.n: success (separately compiled)
declare I.n, declare B.n: overriding error (B vs. I)
declare I.n, invoke I.n: resolution error (Task vs. I) (separately compiled)
declare I.n, invoke B.n: AME (separately compiled)
declare I.o: overriding error (B vs. I)
declare I.o, declare B.o: overriding error (B vs. I)
declare I.o, invoke I.o: (never reaches invocation)
declare I.o, invoke B.o: (never reaches invocation)
Behavior for 'm' and 'n' is mostly* correct, but in the case of 'o':
1) Simply declaring I.o causes an overriding error; this is in contrast to simply declaring A.m or I.n, which cause no error.
2) If we somehow turn off the overriding error, we still need a Task vs. I test to occur when either I.o or B.o is invoked (even though the VM sees the resolved method as the B.o bridge in the latter case).
(* I believe "declare I.n, invoke B.n" should trigger a resolution error, as in "declare A.m, invoke B.m", not an AME...)