Story of a Java 8 Compiler Bug (JDK-8064803)

Story begins with a cleanup done on Hazelcast IQueue interface by one of my colleagues. What’s done actually was just removing unneeded/redundant overridden methods those are already defined in java.util.Queue and java.util.concurrent.BlockingQueue interfaces. It was a very simple cleanup, but…

Full commit diff can be seen here: 2d9eabe82a0a0568d2c92c7d53a5383efb32cf7d#diff-fc20765c5b3aa6eafd6348c5a004447b

Then suddenly, we started to see build errors on our Jenkins CI compatibility builds which are using java 8. There was no compile errors but only a single test using a parameterized queue was failing with java.lang.NoSuchMethodError: com.hazelcast.core.IQueue.poll()Ljava/lang/String; .

Here is the failing test method which has nothing special:

@Test public void testQueueItemListener () throws InterruptedException { final CountDownLatch latch = new CountDownLatch ( 8 ); final String value = "hello" ; final HazelcastInstance instance = createHazelcastInstance (); IQueue < String > queue = instance . getQueue ( "testQueueItemListener" ); queue . addItemListener ( new ItemListener < String >() { public void itemAdded ( ItemEvent < String > itemEvent ) { assertEquals ( value , itemEvent . getItem ()); latch . countDown (); } public void itemRemoved ( ItemEvent < String > itemEvent ) { assertEquals ( value , itemEvent . getItem ()); latch . countDown (); } }, true ); queue . offer ( value ); assertEquals ( value , queue . poll ()); // <---- error is thrown here queue . offer ( value ); assertTrue ( queue . remove ( value )); queue . add ( value ); assertEquals ( value , queue . remove ()); queue . put ( value ); assertEquals ( value , queue . take ()); assertTrue ( latch . await ( 5 , TimeUnit . SECONDS )); assertTrue ( queue . isEmpty ()); }

My gut feeling was saying that, this is certainly a compiler bug. But I needed to prove that using a simple and easily reproducible test case. At the beginning I wrote a dummy implementation of IQueue interface and executed the same test against it. Result was the same failure again.

Then I decided to emulate the Hazelcast IQueue implementation using a few single abstract method interfaces. There were two parent interfaces ParentA and ParentB replacing java.util.Queue and java.util.concurrent.BlockingQueue . And the actual interface Child which is substitute of IQueue . And an empty implementation of Child interface:

public interface ParentA < T > { T process () throws Exception ; } public interface ParentB < T > { T process () throws Exception ; } public interface Child < T > extends ParentA < T >, ParentB < T > { } public class ChildImpl < T > implements Child < T > { @Override public T process () { return null ; } }

Test method was very simple, just instantiate a ChildImpl with String generic type by assigning it to a Child reference and call empty process() method:

public class Test { public static void main ( String [] args ) throws Exception { Child < String > child = new ChildImpl < String >(); String result = child . process (); System . err . println ( result ); } }

Compiled these with javac 8 and ran the main method. Result was as expected: failure!

Exception in thread "main" java.lang.NoSuchMethodError: Child.process()Ljava/lang/String;

When I disassembled the generated bytecode using javap tool ( javap -c Test.class ), output was:

public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: new #2 // class ChildImpl 3: dup 4: invokespecial #3 // Method ChildImpl."<init>":()V 7: astore_1 8: aload_1 9: invokeinterface #4, 1 // InterfaceMethod Child.process:()Ljava/lang/String; 14: astore_2 15: getstatic #5 // Field java/lang/System.err:Ljava/io/PrintStream; 18: aload_2 19: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 22: return }

Code was trying to invoke an interface method which is returning a String , see this line

9: invokeinterface #4, 1 // InterfaceMethod Child.process:()Ljava/lang/String;

Problem was Child interface doesn’t have a process() method returning a String . It’s a generic method and because of type erasure it simply returns a plain Object . Compiler itself should add a cast instruction where needed.

Then I compiled the same interfaces/classes using javac 7. When I disassembled the Test.class again, it was invoking the righ interface method and checking returned type using checkcast bytecode:

public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: new #2 // class failure/ChildImpl 3: dup 4: invokespecial #3 // Method failure/ChildImpl."<init>":()V 7: astore_1 8: aload_1 9: invokeinterface #4, 1 // InterfaceMethod failure/Child.process:()Ljava/lang/Object; 14: checkcast #5 // class java/lang/String 17: astore_2 18: getstatic #6 // Field java/lang/System.err:Ljava/io/PrintStream; 21: aload_2 22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: return }

See these two lines:

9: invokeinterface #4, 1 // InterfaceMethod failure/Child.process:()Ljava/lang/Object; 14: checkcast #5 // class java/lang/String

It was known that everything was OK before we cleaned up overridden interface methods from IQueue interface. To verify that, I added an overriding method declaration to Child interface, which then became:

public interface Child < T > extends ParentA < T >, ParentB < T > { T process () throws Exception ; }

As I was expecting this version generated the correct bytecode and worked fine using java 8 too.

To prevent this issue happening for our java 8 users, we added back some of the overriding generic methods to the IQueue interface with commit 81581a2d11dfcc535c2549a222a8dbd054f8669d. Now our IQueue looks like:

public interface IQueue < E > extends BlockingQueue < E >, BaseQueue < E >, ICollection < E > { /* * Added poll(), poll(long timeout, TimeUnit unit) and take() * methods here to prevent wrong method return type issue when * compiled with java 8. * * For additional details see; * * http://mail.openjdk.java.net/pipermail/compiler-dev/2014-November/009139.html * https://bugs.openjdk.java.net/browse/JDK-8064803 * */ E poll (); E poll ( long timeout , TimeUnit unit ) throws InterruptedException ; E take () throws InterruptedException ; /** * Returns LocalQueueStats for this queue. * LocalQueueStats is the statistics for the local portion of this * queue. * * @return this queue's local statistics. */ LocalQueueStats getLocalQueueStats (); }

I also reported this issue to compiler-dev mailgroup. See http://mail.openjdk.java.net/pipermail/compiler-dev/2014-November/009139.html. My report was replied back by Maurizio Cimadamore promptly. It was definitely a javac bug and he filed an issue on openjdk issue tracker, JDK-8064803. Issue was solved in a few days for JDK-9 target but sadly this bug still exists in javac 8 as of version java 1.8.0_31.