LambdaConversionException с дженериками: ошибка JVM?
У меня есть некоторый код со ссылкой на метод, который прекрасно компилируется и не работает во время выполнения.
Исключением является следующее:
Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class redacted.BasicEntity; not a subtype of implementation type interface redacted.HasImagesEntity
at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233)
at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
at java.lang.invoke.CallSite.makeSite(CallSite.java:289)
Класс, вызывающий исключение:
class ImageController<E extends BasicEntity & HasImagesEntity> {
void doTheThing(E entity) {
Set<String> filenames = entity.getImages().keySet().stream()
.map(entity::filename)
.collect(Collectors.toSet());
}
}
Исключение выдается при попытке разрешить entity::filename
. filename()
объявлено в HasImagesEntity
. Насколько я могу судить, я получаю исключение, потому что стирание E - это BasicEntity
а JVM не рассматривает (не может?) Другие границы для E.
Когда я переписываю ссылку на метод как тривиальную лямбду, все в порядке. Мне кажется действительно подозрительным, что одна конструкция работает так, как ожидалось, и ее семантический эквивалент взрывается.
Может ли это быть в спецификации? Я очень стараюсь найти способ, чтобы это не было проблемой в компиляторе или во время выполнения, и ничего не придумали.
Ответы
Ответ 1
Вот упрощенный пример, который воспроизводит проблему и использует только основные классы Java:
public static void main(String[] argv) {
System.out.println(dummy("foo"));
}
static <T extends Serializable&CharSequence> int dummy(T value) {
return Optional.ofNullable(value).map(CharSequence::length).orElse(0);
}
Ваше предположение верно, реализация JRE-специфики получает целевой метод как MethodHandle
, который не имеет информации о типах общего типа. Поэтому единственное, что он видит, это то, что типы raw не совпадают.
Как и в случае с множеством общих конструкций, на уровне байтового кода требуется тип, который не отображается в исходном коде. Поскольку LambdaMetafactory
явно требует дескриптора прямого метода, ссылка метода, которая инкапсулирует такой тип, не может быть передана как MethodHandle
в factory.
Есть два возможных способа борьбы с ним.
Первое решение состояло в том, чтобы изменить LambdaMetafactory
на доверие MethodHandle
, если тип приемника - interface
, и вставить требуемый тип, сделанный сам по себе в сгенерированном классе лямбда вместо его отклонения. В конце концов, он уже аналогичен параметрам и типам возврата.
В качестве альтернативы, компилятор будет отвечать за создание синтетического вспомогательного метода, инкапсулирующего вызов типа и метода, как если бы вы написали лямбда-выражение. Это не уникальная ситуация. Если вы используете ссылку метода на метод varargs или создание массива, например, например, String[]::new
, они не могут быть выражены в виде ручек прямого метода и заканчиваются синтетическими вспомогательными методами.
В любом случае мы можем считать текущее поведение ошибкой. Но, очевидно, разработчики компилятора и JRE должны договориться о том, каким образом он должен быть обработан, прежде чем мы сможем сказать, на какой стороне находится ошибка.
Ответ 2
Я только что исправил эту проблему в JDK9 и JDK8u45. См. эту ошибку. Для перехода к продвинутым сборкам потребуется немного времени.
Дэн просто указал мне на этот вопрос StackOverflow, поэтому я добавляю эту заметку. Когда вы найдете ошибки, пожалуйста, отправьте их.
Я обратился к этому с помощью того, что компилятор создает мост, как и подход для многих случаев сложных ссылок на методы. Мы также изучаем технические последствия.
Ответ 3
Эта ошибка не полностью исправлена. Я просто столкнулся с LambdaConversionException
в 1.8.0_72 и увидел, что в системе отслеживания ошибок Oracle есть открытые отчеты об ошибках: link1, link2.
(Редактировать: сообщается, что связанные ошибки устранены в JDK 9 b93)
В качестве простого обходного пути я избегаю ручек методов. Так что вместо
.map(entity::filename)
я делаю
.map(entity -> entity.filename())
Вот код для воспроизведения проблемы в Debian 3.11.8-1 x86_64.
import java.awt.Component;
import java.util.Collection;
import java.util.Collections;
public class MethodHandleTest {
public static void main(String... args) {
new MethodHandleTest().run();
}
private void run() {
ComponentWithSomeMethod myComp = new ComponentWithSomeMethod();
new Caller<ComponentWithSomeMethod>().callSomeMethod(Collections.singletonList(myComp));
}
private interface HasSomeMethod {
void someMethod();
}
static class ComponentWithSomeMethod extends Component implements HasSomeMethod {
@Override
public void someMethod() {
System.out.println("Some method");
}
}
class Caller<T extends Component & HasSomeMethod> {
public void callSomeMethod(Collection<T> components) {
components.forEach(HasSomeMethod::someMethod); // <-- crashes
// components.forEach(comp -> comp.someMethod()); <-- works fine
}
}
}
Ответ 4
Я обнаружил, что обходной путь для этого заключался в замене порядка дженериков. Например, используйте class A<T extends B & C>
, где вам нужно получить доступ к методу B
или использовать class A<T extends C & B>
, если вам нужен доступ к методу C
. Конечно, если вам нужен доступ к методам обоих классов, это не сработает. Я нашел это полезным, когда один из интерфейсов был интерфейсом маркера, например Serializable
.
Что касается исправления этого в JDK, единственной информацией, которую я смог найти, были некоторые ошибки в trackjer openjdk, которые помечены как разрешенные в версии 9, что довольно бесполезно.