Какая разница между Foo :: new и() → новым Foo()?

У меня создалось впечатление, что Foo::new - это просто синтаксический сахар для () → new Foo() и они должны вести себя одинаково. Однако, похоже, это не так. Здесь фон:

С Java-8 я использую стороннюю библиотеку с Optional<Foo> foo и этой оскорбительной строкой:

foo.orElseGet(JCacheTimeZoneCache::new);

JCacheTimeZoneCache использует в своем конструкторе что-то из дополнительной библиотеки JCache, которой у меня нет в моем классе. С отладчиком я проверил, что foo не является нулевым, поэтому он фактически не должен создавать экземпляр JCacheTimeZoneCache, и поэтому недостающая библиотека JCache не должна быть проблемой. Однако он взрывается с помощью stacktrace, жалующегося на недостающую библиотеку JCache:

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

Сначала я удивлен этой ошибкой, поскольку код не создает экземпляр JCacheTimeZoneCache. Хорошо, добавив JCache в путь класса, это исправит. Но автор библиотеки сделал совсем другое решение:

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

Теперь я полностью удивлен? У меня на самом деле два вопроса:

  1. Почему JCacheTimeZoneCache :: new вызывает это исключение в первую очередь, когда конструктор никогда не вызывался?
  2. Почему () → new JCacheTimeZoneCache() исправить эту проблему?

Ответы

Ответ 1

Эти 2 могут быть реализованы по-разному, в зависимости от используемого java-компилятора и в каком случае (я не сузил это, но он действительно является частью реализации любым способом).

Вы можете проверить это, посмотрев на выход javap -v <enclosing class> и посмотрев таблицу BootstrapMethod. Компилятор может сгенерировать это для случая с примером метода:

  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;

В частности, что важно для MyClass."<init>":()V Это означает, что конструктор класса, используемый в выражении MyClass::new, просматривается напрямую.


За:

JCacheTimeZoneCache::new

Сгенерированная invokedynamic ищет конструктор в классе JCacheTimeZoneCache и переносит ее в функциональный интерфейс (используя LambdaMetafactory).

За:

() -> new JCacheTimeZoneCache()

Все компиляторы Java, на которые я сейчас смотрел, генерируют синтетический статический метод в охватывающем классе, содержащем лямбда-код, а затем он завернут в функциональный интерфейс сгенерированной invokedynamic.

Разница заключается в том, что для первого требуется JCacheTimeZoneCache класса JCacheTimeZoneCache, а для второго требуется только загрузка входящего класса (который предположительно уже загружен). Только когда на самом деле выполняется JCacheTimeZoneCache, требуется загрузка JCacheTimeZoneCache, потому что это когда нужно сначала.


Поскольку это "исправление" основано на деталях реализации, оно не очень хорошее. В будущем может произойти изменение в будущем, которое влияет на то, как генерируются незахватывающие lambdas (включая конструкторы): JDK-8186216, которые могут снова сломать код.