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

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.

Когда я переписываю ссылку на метод как тривиальную лямбду, все в порядке. Мне кажется действительно подозрительным, что одна конструкция работает так, как ожидалось, и ее семантический эквивалент взрывается.

Может ли это быть в спецификации? Я очень стараюсь найти способ, чтобы это не было проблемой в компиляторе или во время выполнения, и ничего не придумали.

4b9b3361

Ответ 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, что довольно бесполезно.