Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enum, interfaces and (Java 8) lambdas: code compiles but fails at runtime; is this expected?

Tags:

JDK is Oracle' JDK 1.8u65 but the problem has been seen with "as low as" 1.8u25 as well.

Here is the full SSCCE:

public final class Foo {     private interface X     {         default void x()         {         }     }      private enum E1         implements X     {         INSTANCE,         ;     }      private enum E2         implements X     {         INSTANCE,         ;     }      public static void main(final String... args)     {         Stream.of(E1.INSTANCE, E2.INSTANCE).forEach(X::x);     } } 

This code compiles; but it fails at runtime:

Exception in thread "main" java.lang.BootstrapMethodError: call site initialization exception     at java.lang.invoke.CallSite.makeSite(CallSite.java:341)     at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307)     at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297)     at com.github.fge.grappa.debugger.main.Foo.main(Foo.java:38)     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)     at java.lang.reflect.Method.invoke(Method.java:497)     at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Enum; not a subtype of implementation type interface com.github.fge.grappa.debugger.main.Foo$X     at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233)     at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)     at java.lang.invoke.CallSite.makeSite(CallSite.java:302)     ... 8 more 

Fixing it in code is "easy"; in the main method, you just have to:

// Note the <X> Stream.<X>of(E1.INSTANCE, E2.INSTANCE).forEach(X::x); 

EDIT There is in fact a second way, as mentioned in the accepted answer... Replace the method reference with a lambda:

Stream.of(E1.INSTANCE, E2.INSTANCE).forEach(x -> x.x()); 

So, uh. What happens here? Why does the initial code compile in the first place? I'd have expected the compiler to notice that the method reference was not on anything Enum<?> but on X, but no...

What am I missing? Is this a bug in the compiler? A misunderstanding of mine?

like image 304
fge Avatar asked Nov 25 '15 20:11

fge


1 Answers

It seems you've hit JDK-8141508, which is indeed a bug of javac when dealing with intersection types and method-references. It is scheduled to be fixed in Java 9.

Quoting a mail from Remi Forax:

javac has trouble with intersection type that are target type of a lambda and method reference, Usually when there is an intersection type, javac substitute it by the first type of the intersection type and add cast when necessary.

Let suppose we have this code,

public class Intersection {       interface I {       }       interface J {           void foo();       }        static <T extends I & J> void bar(T t) {           Runnable r = t::foo;       }         public static void main(String[] args) {           class A implements I, J { public void foo() {} }           bar(new A());       }   } 

Currently, javac generates a method reference on J::foo with an invokedynamic that takes an I as parameter, hence it fails at runtime. javac should de-sugar t::foo into a lambda that take an I and then add a cast to J like for a call to a method of an intersection type.

So the workaround is to use a lambda instead,

Runnable r = t -> t.foo(); 

I've already seen this bug somewhere but was not able to find a corresponding bug report in the database :(

In your code, the Stream created by Stream.of(E1.INSTANCE, E2.INSTANCE) is of type Stream<Enum<?>&Foo.X>, which combines all the elements of the bug: intersecting types and method-references.

As noted by Remi Forax, a work-around would be:

Stream.of(E1.INSTANCE, E2.INSTANCE).forEach(x -> x.x()); 

i.e. using an explicit lambda expression instead of a method-reference.

like image 146
Tunaki Avatar answered Sep 28 '22 22:09

Tunaki