Подтвердить что ты не робот

Выражение Lambda не выполняется с помощью 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.

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


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

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {

    @Override
    void apply(SomeApplicable self);
}

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

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

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
4b9b3361

Ответ 1

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

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

Анонимный тест класса $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).

Lambda

Экземпляр lambda получается путем выполнения 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

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

  • Тип метода реализованного интерфейса: void (a.Applicable);
  • Прямой методHandle для реализации;
  • Эффективный метод. Тип выражения лямбда: void (a.SomeApplicable).

Все эти аргументы разрешаются во время процесса invokedynamic bootstrap (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, и этот код работал без ошибок.