21

I was under the impression that Foo::new is just syntactic sugar for () -> new Foo() and they should behave identically. However it seems not to be the case. Here's the background:

With Java-8 I use a third party library which has an Optional<Foo> foo and this offending line:

foo.orElseGet(JCacheTimeZoneCache::new);

JCacheTimeZoneCache uses in its constructor something from the optional JCache library, which I have not in my class path. With a debugger I verified that foo is not null, so it should actually never instantiate a JCacheTimeZoneCache instance and therefore the missing JCache library should not be an issue. However it does explode with stacktrace complaining about the missing JCache library:

Caused by: java.lang.BootstrapMethodError: java.lang.IllegalAccessError: no such constructor: net.fortuna.ical4j.util.JCacheTimeZoneCache.<init>()void/newInvokeSpecial
    at net.fortuna.ical4j.model.TimeZoneLoader.cacheInit(TimeZoneLoader.java:275) ~[ical4j-3.0.0.jar:na]
    at net.fortuna.ical4j.model.TimeZoneLoader.<init>(TimeZoneLoader.java:81) ~[ical4j-3.0.0.jar:na]
    at net.fortuna.ical4j.model.TimeZoneRegistryImpl.<init>(TimeZoneRegistryImpl.java:125) ~[ical4j-3.0.0.jar:na]
    at net.fortuna.ical4j.model.TimeZoneRegistryImpl.<init>(TimeZoneRegistryImpl.java:116) ~[ical4j-3.0.0.jar:na]
    at net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory.createRegistry(DefaultTimeZoneRegistryFactory.java:48) ~[ical4j-3.0.0.jar:na]
    at net.fortuna.ical4j.data.CalendarBuilder.<init>(CalendarBuilder.java:105) ~[ical4j-3.0.0.jar:na]
    at de.malkusch.trashcollection.infrastructure.schedule.ical.VEventRepository.downloadVEvents(VEventRepository.java:46) ~[classes/:na]
    at de.malkusch.trashcollection.infrastructure.schedule.ical.VEventRepository.<init>(VEventRepository.java:35) ~[classes/:na]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_172]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_172]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_172]
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_172]
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:170) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
    ... 80 common frames omitted
Caused by: java.lang.IllegalAccessError: no such constructor: net.fortuna.ical4j.util.JCacheTimeZoneCache.<init>()void/newInvokeSpecial
    at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(MethodHandleNatives.java:483) ~[na:1.8.0_172]
    ... 93 common frames omitted
Caused by: java.lang.NoClassDefFoundError: javax/cache/configuration/Configuration
    at java.lang.invoke.MethodHandleNatives.resolve(Native Method) ~[na:1.8.0_172]
    at java.lang.invoke.MemberName$Factory.resolve(MemberName.java:975) ~[na:1.8.0_172]
    at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1000) ~[na:1.8.0_172]
    at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:1394) ~[na:1.8.0_172]
    at java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(MethodHandles.java:1750) ~[na:1.8.0_172]
    at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(MethodHandleNatives.java:477) ~[na:1.8.0_172]
    ... 93 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.cache.configuration.Configuration
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_172]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_172]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_172]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_172]
    ... 99 common frames omitted

First I am surprised by this error, as the code does not instantiate JCacheTimeZoneCache at all. Ok, putting JCache into the class path would fix that. But the author of the library did a very different fix:

foo.orElseGet(() -> new JCacheTimeZoneCache());

Now I'm totally surprised? I have actually two questions:

  1. Why did JCacheTimeZoneCache::new cause that exception in the first place, when the constructor was never called?
  2. Why did () -> new JCacheTimeZoneCache() fix that issue?
Zabuzard
  • 25,064
  • 8
  • 58
  • 82
Markus Malkusch
  • 7,738
  • 2
  • 38
  • 67

1 Answers1

11

These 2 might be implemented differently, depending on the java compiler you're using and in what case (I haven't narrowed this down, but it's really an implementation detail any ways).

You can check this by looking at the output of javap -v <enclosing class>, and looking at the BootstrapMethod table. The compiler might generate this for the method reference case:

  1: #22 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #23 ()Ljava/lang/Object;
      #27 REF_newInvokeSpecial MyClass."<init>":()V
      #25 ()LMyClass;

Specifically, what's important is the MyClass."<init>":()V. Which means that the constructor of the class used in a MyClass::new expression is being looked up directly.


For:

JCacheTimeZoneCache::new

The generated invokedynamic instruction looks up the constructor in the JCacheTimeZoneCache class directly, and wraps it in a functional interface (using LambdaMetafactory).

For:

() -> new JCacheTimeZoneCache()

All of the Java compilers that I've looked at so far generate a synthetic static method in the enclosing class containing the lambda's code, and then that is wrapped in a functional interface by the generated invokedynamic.

The difference being that for the first, the loading of the JCacheTimeZoneCache class is required, and for the second only the loading of the enclosing class (which is presumably already loaded) is required. Only when the lambda is actually executed the loading of JCacheTimeZoneCache is required, because that's when it's first needed.


Since this 'fix' is based on an implementation detail, it's not a very good one. There might be a change in the future which affects how non-capturing lambdas (including constructors) are generated: JDK-8186216 which could break the code again.

Jorn Vernee
  • 31,735
  • 4
  • 76
  • 93
  • Thanks for the clarification. Is that difference an implementation detail or actual documented behaviour? – Markus Malkusch Jun 30 '18 at 09:16
  • @MarkusMalkusch Yes that is an implementation detail. In fact, when I went to add `javap` output to the answer I found out that the eclipse compiler does this, but not `javac`. It looks like `javac` generates a synthetic method in both cases. (I'll add that to the answer). – Jorn Vernee Jun 30 '18 at 09:18
  • @MarkusMalkusch Or actually, or you using Eclipse? Otherwise this answer is not correct and I should delete it. – Jorn Vernee Jun 30 '18 at 09:19
  • I would add that the "fix" that was made was in fact hiding the bug rather than fixing it. The proper fix would be to add the required classes to the classpath. – JB Nizet Jun 30 '18 at 09:21
  • Yes I was using Eclipse, but also compiling it with javac results in the same issue. Ah wait, you mean with javac the fix is not fixed… I'll check that. – Markus Malkusch Jun 30 '18 at 09:22
  • un-deleting this since it seems that the compiler seems to only generated a synthetic method around the method reference when I was using a local class. – Jorn Vernee Jun 30 '18 at 10:02
  • @JBNizet I think the idea is similar to how frameworks such as Spring work - they have lots of code that is triggered when certain dependencies are present and ignored otherwise. I guess (haven't actually looked) that this code activated the cache if the library is present. – Boris the Spider Jun 30 '18 at 10:02
  • 2
    @BoristheSpider I see. Then the fix shouldn't rely on such an implementation detail of lambdas. Instead, it should check if a specific implementation is provided, then check if the default implementation is in the classpath, and finally return it or fail with a clear error message. The next developer maintaining this code will see the suggestion of the IDE to change this to a method reference, and will reintroduce the bug. – JB Nizet Jun 30 '18 at 10:08
  • @JBNizet totally agree. – Boris the Spider Jun 30 '18 at 10:12
  • @MarkusMalkusch is is good to know that method references in java are not just different syntax for lambda, like consumer `System.out::print` will link to existing `out` instance and any change using `System.setOut` will not affect that consumer, also i would throw error on lambda creation if `out` was null (instead of execution) – GotoFinal Jun 30 '18 at 11:03