Лямбда-выражение завершается ошибкой java.lang.BootstrapMethodError во время выполнения.

В одном пакете (a) у меня два функциональных интерфейса:

package a;

@FunctionalInterface
interface Applicable<A extends Applicable<A>> {

    void apply(A self);
}

-

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
}

Метод apply в суперинтерфейсе принимает self как A, потому что в противном случае, если бы вместо него использовался Applicable<A>, тип не был бы виден за пределами пакета, и, следовательно, метод не мог бы быть реализован.

В другом пакете (b) у меня есть следующий класс Test:

package b;

import a.SomeApplicable;

public class Test {

    public static void main(String[] args) {

        // implement using an anonymous class
        SomeApplicable a = new SomeApplicable() {
            @Override
            public void apply(SomeApplicable self) {
                System.out.println("a");
            }
        };
        a.apply(a);

        // implement using a lambda expression
        SomeApplicable b = (SomeApplicable self) -> System.out.println("b");
        b.apply(b);
    }
}

Первая реализация использует анонимный класс и работает без проблем. Второй, с другой стороны, компилируется нормально, но во время выполнения происходит сбой, вызывающий java.lang.BootstrapMethodError, вызванный java.lang.IllegalAccessError, когда он пытается получить доступ к интерфейсу Applicable.

Exception in thread "main" java.lang.BootstrapMethodError: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
    at b.Test.main(Test.java:19)
Caused by: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
    ... 1 more

Я думаю, было бы разумнее, если бы лямбда-выражение работало так же, как анонимный класс, или выдавало бы ошибку времени компиляции. Так что мне просто интересно, что здесь происходит.


Я попытался удалить суперинтерфейс и объявить метод в SomeApplicable следующим образом:

package a;

@FunctionalInterface
public interface SomeApplicable {

    void apply(SomeApplicable self);
}

Это, очевидно, заставляет его работать, но позволяет нам увидеть, что отличается в байт-коде.

Синтетический метод lambda$0, скомпилированный из лямбда-выражения, кажется идентичным в обоих случаях, но я заметил одно различие в аргументах метода в методах начальной загрузки.

Bootstrap methods:
  0 : # 58 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:
        #59 (La/Applicable;)V
        #62 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
        #63 (La/SomeApplicable;)V

#59 меняется с (La/Applicable;)V на (La/SomeApplicable;)V.

Я действительно не знаю, как работает лямбда-метафабрика, но я думаю, что это может быть ключевым отличием.


Я также попытался явно объявить метод apply в SomeApplicable следующим образом:

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {

    @Override
    void apply(SomeApplicable self);
}

Теперь метод apply(SomeApplicable) действительно существует, и компилятор генерирует метод моста для apply(Applicable). Все равно такая же ошибка вылетает во время выполнения.

На уровне байт-кода теперь используется LambdaMetafactory.altMetafactory вместо LambdaMetafactory.metafactory:

Bootstrap methods:
  0 : # 57 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
        #58 (La/SomeApplicable;)V
        #61 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
        #62 (La/SomeApplicable;)V
        #63 4
        #64 1
        #66 (La/Applicable;)V

person Bubletan    schedule 26.10.2016    source источник
comment
Не могли бы вы предоставить полную трассировку стека? Выдача Error звучит очень подозрительно.   -  person GhostCat    schedule 26.10.2016
comment
@GhostCat В трассировке стека мало что можно увидеть: Caused by: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test в строке лямбда-выражения.   -  person Bubletan    schedule 26.10.2016
comment
Учитывая ваше описание, я не уверен, что это закрытие DUP законно. На вашем месте я бы создал полный минимальный жизнеспособный пример и добавил его в ваш вопрос. Если вы можете показать, что одна часть кода, скомпилированная из одного файла, приводит к этой ошибке, то этот DUP не соответствует; и вы должны попросить открыть снова.   -  person GhostCat    schedule 26.10.2016
comment
@GhostCat Я не думаю, что эту ошибку можно получить без двух пакетов, суперинтерфейс не должен быть виден.   -  person Bubletan    schedule 26.10.2016
comment
Я даже не могу это скомпилировать. Я получаю жалобу The type Applicable<SomeApplicable> from the descriptor computed for the target context is not visible here.   -  person dcsohl    schedule 26.10.2016
comment
Все еще. Если вы можете показать, что достаточно скопировать ваш ввод в два класса, и все перестанет работать, то вы правы. В противном случае вы действительно имеете дело с некоторыми несовместимыми файлами классов.   -  person GhostCat    schedule 26.10.2016
comment
И я бы сказал, что многофайловые MCVE приемлемы - важной частью является минимальная часть... минимальная не обязательно означает однофайловую, но это означает, что не нужно заполнять мой браузер. кеш.   -  person dcsohl    schedule 26.10.2016
comment
Я отредактировал пример, чтобы его можно было скопировать. Это три файла, два в пакете a и один в пакете b.   -  person Bubletan    schedule 26.10.2016
comment
Я снова открою. Я могу воспроизвести с javac, не с Eclipse, возможно, ошибка.   -  person Sotirios Delimanolis    schedule 26.10.2016


Ответы (1)


Насколько я вижу, JVM все делает правильно.

Когда метод apply объявлен в Applicable, но не в SomeApplicable, анонимный класс должен работать, а лямбда — нет. Давайте рассмотрим байт-код.

Анонимный класс Test$1

public void apply(a.SomeApplicable);
  Code:
     0: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #3    // String a
     5: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return

public void apply(a.Applicable);
  Code:
     0: aload_0
     1: aload_1
     2: checkcast     #5    // class a/SomeApplicable
     5: invokevirtual #6    // Method apply:(La/SomeApplicable;)V
     8: return

javac генерирует как реализацию метода интерфейса apply(Applicable), так и переопределенный метод apply(SomeApplicable). Ни один из методов не ссылается на недоступный интерфейс Applicable, кроме как в сигнатуре метода. То есть интерфейс Applicable не разрешен (JVMS §5.4.3) в любом месте кода анонимного класса.

Обратите внимание, что apply(Applicable) может быть успешно вызван из Test, поскольку типы в сигнатуре метода не разрешаются во время разрешения инструкции invokeinterface (JVMS §5.4.3.4).

лямбда

Экземпляр лямбда получается путем выполнения invokedynamic байт-код с методом начальной загрузки LambdaMetafactory.metafactory:

BootstrapMethods:
  0: #36 invokestatic java/lang/invoke/LambdaMetafactory.metafactory
    Method arguments:
      #37 (La/Applicable;)V
      #38 invokestatic b/Test.lambda$main$0:(La/SomeApplicable;)V
      #39 (La/SomeApplicable;)V

Статические аргументы, используемые для построения лямбды:

  1. MethodType реализованного интерфейса: void (a.Applicable);
  2. Направить MethodHandle в реализацию;
  3. Действующий тип метода лямбда-выражения: void (a.SomeApplicable).

Все эти аргументы разрешаются в процессе invokedynamic начальной загрузки (JVMS §5.4.3.6).

Теперь ключевой момент: для разрешения MethodType все классы и интерфейсы, указанные в его дескрипторе метода, разрешаются (JVMS §5.4.3.5). В частности, JVM пытается разрешить a.Applicable от имени класса Test и терпит неудачу с IllegalAccessError. Затем, согласно спецификации invokedynamic ошибка заключена в BootstrapMethodError.

Мостовой метод

Чтобы обойти IllegalAccessError, вам нужно явно добавить метод моста в общедоступный интерфейс SomeApplicable:

public interface SomeApplicable extends Applicable<SomeApplicable> {
    @Override
    void apply(SomeApplicable self);
}

В этом случае лямбда реализует метод apply(SomeApplicable) вместо apply(Applicable). Соответствующая инструкция invokedynamic будет ссылаться на (La/SomeApplicable;)V MethodType, который будет успешно разрешен.

Примечание: недостаточно изменить только SomeApplicable интерфейс. Вам придется перекомпилировать Test с новой версией SomeApplicable, чтобы сгенерировать invokedynamic с правильными типами методов. Я проверил это на нескольких JDK от 8u31 до последней версии 9-ea, и рассматриваемый код работал без ошибок.

person apangin    schedule 30.10.2016
comment
Обходной путь метода моста, похоже, не работает, если он скомпилирован с помощью Eclipse. Теперь, когда я попробовал это с javac, он работал, как и ожидалось. По какой-то причине компилятор Eclipse использует altMetafactory с флагом BRIDGES(La/Applicable;)V в качестве типа метода моста), что вызывает ту же ошибку. Я придумал еще один простой обходной путь: объявить Applicable как Applicable<A extends Object & Applicable<A>>. Тогда тип параметра будет общедоступным, поскольку он стирается до Object. - person Bubletan; 31.10.2016
comment
В любом случае, если JVM все делает правильно, мне это кажется проблемой компилятора. Сообщить об ошибке или использовать какое-либо возможное обходное решение было бы лучшим вариантом, чем просто молча принимать недопустимый код. - person Bubletan; 31.10.2016