Выражение 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
Ответы
Ответ 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, и этот код работал без ошибок.