Pregunta El comportamiento genérico es diferente en JDK 8 y 9


La siguiente clase simple (repo para reproducirlo):

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"))));
  }
}

El comportamiento depende de la versión JDK:

  • Se compila correctamente en JDK <= 8 (probado con 7 y 8)
  • La compilación falla utilizando JDK 9+ (probado con 9, 10 y 11 EA)

Con el siguiente error:

[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))

¿Este es un cambio esperado en JDK 9 o es un error?

Podría extraer los matchers de las variables tipadas de esta manera, y funcionaría:

    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)));

Pero aún la pregunta es: ¿es correcto? javac <= 8 es capaz de inferir tipos, pero no en 9+?


32
2018-06-19 12:20


origen


Respuestas:


Después de algunas investigaciones, creo que podemos descartar esto como un problema de Junit o Hamcrest. De hecho, esto parece ser un error JDK. El siguiente código no se compilará en 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

Turing esto en un MCVE que no usa bibliotecas:

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());
}

Se puede lograr un efecto similar usando una sola variable en bar lo que da como resultado una restricción de igualdad de límites superiores en oposición a una menor:

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

Parece que cuando el JDK intenta racionalizar ? super U no puede encontrar la clase de comodín apropiada para usar. Aún más interesante, si especifica completamente el tipo de foo, entonces el compilador realmente tendrá éxito. Esto es válido tanto para MCVE como para la publicación original:

// 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());

Y al igual que en el caso que usted presentó, dividir la ejecución en múltiples líneas producirá los resultados correctos:

A<B<? super D>> a1 = foo();
A<B<? super D>> a2 = foo();
C<B<? super D>> c = bar(a1, a2);

Debido a que hay varias formas de escribir este código que debería ser funcionalmente equivalente, y dado que solo algunos de ellos compilan, mi conclusión es que este no es el comportamiento previsto del JDK. Hay un error en algún lugar dentro de la evaluación de los comodines que tienen un super ligado.

Mi recomendación sería compilar código existente contra JDK 8, y para código más nuevo que requiera JDK> 8, para especificar completamente el valor genérico.


10
2018-06-28 05:11



Creé un MCVE diferente que muestra una diferencia en la inferencia de tipo:

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"));
    }
}

Pases de compilación JDK8, JDK10 da:

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"));

Entonces parece que JDK10 tiene una resolución de errores N a List<? super String> en

Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second)

cuando llamas

anyOf(Matcher<List<? super String>>, Matcher<List<? super String>>)

Recomendaría informar este problema a OpenJDK (relacionando el problema aquí) y posiblemente informar el problema al proyecto de Hamcrest.


1
2018-06-29 05:45