Java Casting: Java 11 генерирует LambdaConversionException, а 1.8 - нет

следующий код прекрасно работает в виртуальной LambdaConversionException Java 1.8, но создает LambdaConversionException при выполнении в LambdaConversionException Java 11. Где разница и почему она так себя ведет?


Код:

public void addSomeListener(Component comp){
    if(comp instanceof HasValue) {
        ((HasValue<?,?>) comp).addValueChangeListener(evt -> {
            //do sth with evt
        });
    }
}

HasValue Javadoc

Исключение (только V11):

Caused by: java.lang.invoke.LambdaConversionException: Type mismatch
for instantiated parameter 0: class java.lang.Object is not a subtype
of interface com.vaadin.flow.component.HasValue$ValueChangeEvent
    at java.base/java.lang.invoke.AbstractValidatingLambdaMetafactory.checkDescriptor(AbstractValidatingLambdaMetafactory.java:308)
    at java.base/java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:294)
    at java.base/java.lang.invoke.LambdaMetafactory.altMetafactory(LambdaMetafactory.java:503)
    at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:138)
    ... 73 more

Временное решение:

ValueChangeListener<ValueChangeEvent<?>> listener = evt -> {
    // do sth with evt
};
((HasValue<?,?>) comp).addValueChangeListener(listener);

система:
ОС: Windows 10
IDE: Eclipse 2018-12 (4.10.0)
Java (компиляция): JDK 1.8.0_201
Java (веб-сервер): JDK 11.0.2
Веб-сервер: Wildfly 15

Ответ 1

TL; DR Компилятор Eclipse генерирует сигнатуру метода для лямбда-экземпляра, который недопустим в соответствии со спецификацией. Из-за того, что в JDK 9 добавлен дополнительный код проверки типов для лучшего применения спецификации, неправильная подпись теперь вызывает исключение при работе на Java 11.


Проверено с Eclipse 2019-03, а также с этим кодом:

public class Main {    
    public static void main(String[] args) {
        getHasValue().addValueChangeListener(evt -> {});
    }

    public static HasValue<?, ?> getHasValue() {
        return null;
    }    
}

interface HasValue<E extends HasValue.ValueChangeEvent<V>,V> {    
    public static interface ValueChangeEvent<V> {}    
    public static interface ValueChangeListener<E extends HasValue.ValueChangeEvent<?>> {
        void valueChanged(E event);
    }    
    void addValueChangeListener(HasValue.ValueChangeListener<? super E> listener);
}

Даже при использовании null в качестве получателя код завершается ошибкой при загрузке с той же ошибкой.

Используя javap -v Main мы можем видеть, в чем проблема. Я вижу это в таблице BoostrapMethods:

BootstrapMethods:
  0: #48 REF_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:
      #50 (Lmain/HasValue$ValueChangeEvent;)V
      #53 REF_invokeStatic main/Main.lambda$0:(Ljava/lang/Object;)V
      #54 (Ljava/lang/Object;)V

Обратите внимание, что последний аргумент (константа # 54) это (Ljava/lang/Object;)V, тогда как javac генерирует (Lmain/HasValue$ValueChangeEvent;)V то есть сигнатура метода, которую Eclipse хочет использовать для лямбды, отличается от той, что хочет использовать javac.

Если требуемая сигнатура метода является стиранием целевого метода (что, по-видимому, имеет место), то правильная сигнатура метода действительно (Lmain/HasValue$ValueChangeEvent;)V поскольку это стирание целевого метода, а именно:

void valueChanged(E event);

Где E - E extends HasValue.ValueChangeEvent<?>, Так что он будет удален в HasValue.ValueChangeEvent.

Кажется, что проблема связана с ECJ, и, похоже, она была раскрыта JDK-8173587 (ревизия) (К сожалению, это похоже на частный тикет.), Который добавляет дополнительные проверки типов для проверки того, что тип метода SAM действительно совместим с экземпляром метода типа. В соответствии с документацией LambdaMetafactory::metafactory экземплярный тип метода должен быть одинаковым или специализация типа метода SAM:

instantiatedMethodType - тип подписи и возвращаемого значения, который должен динамически применяться во время вызова. Это может быть то же самое, что и samMethodType, или может быть его специализацией.

который тип метода, сгенерированный ECJ, очевидно, не является, так что это приводит к исключению. (хотя, если честно, я не вижу нигде определенного определения, что представляет собой "специализацию" в этом случае). Я сообщил об этом в багзилле Eclipse здесь: https://bugs.eclipse.org/bugs/show_bug.cgi?id=546161

Я предполагаю, что это изменение было сделано где-то в JDK 9, так как исходный код на тот момент уже был модульным, а дата пересмотра довольно ранняя (февраль 2017 года).

Поскольку javac генерирует правильную сигнатуру метода, вы можете переключиться на нее в качестве временного решения.