Поведение дженериков отличается в 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.