Поведение дженериков отличается в JDK 8 и 9
Следующий простой класс (repo для его воспроизведения):
import static org.hamcrest.*;
import static org.junit.Assert.assertThat;
import java.util.*;
import org.junit.Test;
public class TestGenerics {
@Test
public void thisShouldCompile() {
List<String> myList = Arrays.asList("a", "b", "c");
assertThat("List doesn't contain unexpected elements", myList, not(anyOf(hasItem("d"), hasItem("e"), hasItem("f"))));
}
}
Поведение зависит от версии JDK:
- Правильно компилируется в JDK <= 8 (тестируется с 7 и 8)
- Компиляция не выполняется с использованием JDK 9+ (тестируется с 9, 10 и 11 EA)
Со следующей ошибкой:
[ERROR] /tmp/jdk-issue-generics/src/test/java/org/alostale/issues/generics/TestGenerics.java:[17,17] no suitable method found for assertThat(java.lang.String,java.util.List<java.lang.String>,org.hamcrest.Matcher<java.lang.Iterable<? super java.lang.Object>>)
method org.junit.Assert.<T>assertThat(java.lang.String,T,org.hamcrest.Matcher<? super T>) is not applicable
(inference variable T has incompatible bounds
upper bounds: java.lang.String,java.lang.Object
lower bounds: capture#1 of ? super T?,capture#2 of ? super java.lang.Object,capture#3 of ? super java.lang.Object,java.lang.Object,java.lang.String,capture#4 of ? super T?)
method org.junit.Assert.<T>assertThat(T,org.hamcrest.Matcher<? super T>) is not applicable
(cannot infer type-variable(s) T
(actual and formal argument lists differ in length))
Это ожидаемое изменение в JDK 9 или ошибка?
Таким образом, я мог бы собрать шаблонов для типизированных переменных, и это сработает:
Matcher<Iterable<? super String>> m1 = hasItem("d");
Matcher<Iterable<? super String>> m2 = hasItem("e");
Matcher<Iterable<? super String>> m3 = hasItem("f");
assertThat(myList, not(anyOf(m1, m2, m3)));
Но все же возникает вопрос: правильно ли javac
<= 8 способен выводить типы, но не в 9+?
Ответы
Ответ 1
После некоторых исследований я считаю, что мы можем исключить это как проблему Junit или hamcrest. Действительно, это похоже на ошибку JDK. Следующий код не будет компилироваться в JDK> 8:
AnyOf<Iterable<? super String>> matcher = CoreMatchers.anyOf(
CoreMatchers.hasItem("d"), CoreMatchers.hasItem("e"), CoreMatchers.hasItem("f"));
Error:(23, 63) java: incompatible types: inference variable T has incompatible bounds
equality constraints: java.lang.String
lower bounds: java.lang.Object,java.lang.String
Тьюринг этого в MCVE, который не использует библиотеки:
class Test {
class A<S> { } class B<S> { } class C<S> { } class D { }
<T> A<B<? super T>> foo() { return null; }
<U> C<U> bar(A<U> a1, A<? super U> a2) { return null; }
C<B<? super D>> c = bar(foo(), foo());
}
Аналогичный эффект может быть достигнут с использованием одной переменной в bar
что приводит к ограничению равенства сверху, а не к нижнему:
class Test {
class A<S> { } class B<S> { } class C<S> { } class D { }
<T> A<B<? super T>> foo() { return null; }
<U> C<U> bar(A<? super U> a) { return null; }
C<B<? super D>> c = bar(foo());
}
Error:(21, 28) java: incompatible types: inference variable U has incompatible bounds
equality constraints: com.Test.B<? super com.Test.D>
upper bounds: com.Test.B<? super capture#1 of ? super com.Test.D>,java.lang.Object
Похоже, когда JDK пытается рационализировать ? super U
? super U
не может найти подходящий класс подстановочных знаков для использования. Еще более интересно, если вы полностью укажете тип для foo
, тогда компилятор действительно добьется успеха. Это справедливо как для MCVE, так и для исходного сообщения:
// This causes compile to succeed even though an IDE will call it redundant
C<B<? super D>> c = bar(this.<D>foo(), this.<D>foo());
И точно так же, как и в случае, который вы представили, разбить выполнение на несколько строк приведет к правильным результатам:
A<B<? super D>> a1 = foo();
A<B<? super D>> a2 = foo();
C<B<? super D>> c = bar(a1, a2);
Поскольку существует несколько способов написания этого кода, который должен быть функционально эквивалентен, и учитывая, что только некоторые из них компилируются, я пришел к выводу, что это не предполагаемое поведение JDK. Существует ошибка где - то в оценке групповых символов, которые имеют super
предел.
Моя рекомендация заключалась бы в компиляции существующего кода с JDK 8 и для более нового кода, требующего JDK> 8, чтобы полностью указать общее значение.
Ответ 2
Я создал другой MCVE, показывающий разницу в типе вывода:
import java.util.Arrays;
import java.util.List;
public class Example {
public class Matcher<T> {
private T t;
public Matcher(T t) {
this.t = t;
}
}
public <N> Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second) {
return first;
}
public <T> Matcher<List<? super T>> hasItem1(T item) {
return new Matcher<>(Arrays.asList(item));
}
public <T> Matcher<List<? super T>> hasItem2(T item) {
return new Matcher<>(Arrays.asList(item));
}
public void thisShouldCompile() {
Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));
}
}
JDK8 компилирует проходы, JDK10 дает:
Example.java:27: error: incompatible types: Example.Matcher<List<? super Object>> cannot be converted to Example.Matcher<List<? super String>>
Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));
Итак, кажется, что JDK10 имеет ошибку, разрешающую N
для List<? super String>
List<? super String>
in
Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second)
при звонке
anyOf(Matcher<List<? super String>>, Matcher<List<? super String>>)
Я бы рекомендовал сообщить об этой проблеме OpenJDK (связывая проблему здесь) и, возможно, сообщить о проблеме в проект hamcrest.