Pertanyaan Perilaku generik berbeda dalam JDK 8 dan 9


Kelas sederhana berikut (repo untuk mereproduksinya):

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

Perilaku tergantung pada versi JDK:

  • Menyusun dengan benar di JDK <= 8 (diuji dengan 7 dan 8)
  • Kompilasi gagal menggunakan JDK 9+ (diuji dengan 9, 10, dan 11 EA)

Dengan kesalahan berikut:

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

Apakah ini beberapa perubahan yang diharapkan dalam JDK 9 atau itu bug?

Saya bisa mengekstrak matchers ke variabel yang diketik dengan cara ini, dan itu akan bekerja:

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

Namun pertanyaannya tetap: apakah itu benar javac <= 8 dapat menyimpulkan jenis, tetapi tidak dalam 9+?


32
2018-06-19 12:20


asal


Jawaban:


Setelah beberapa penelitian, saya yakin kami bisa mengatasinya sebagai masalah Junit atau hamcrest. Memang, ini sepertinya adalah bug JDK. Kode berikut tidak akan dikompilasi dalam 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

Memproses ini menjadi MCVE yang tidak menggunakan pustaka:

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

Efek serupa dapat dicapai dengan menggunakan satu variabel di bar yang menghasilkan batasan kesamaan kesetaraan sebagai lawan yang lebih rendah:

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

Sepertinya ketika JDK berusaha untuk merasionalisasi ? super U gagal menemukan kelas wildcard yang tepat untuk digunakan. Yang lebih menarik, jika Anda sepenuhnya menentukan jenisnya foo, maka kompilator akan benar-benar berhasil. Ini berlaku untuk MCVE dan pos asli:

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

Dan seperti dalam kasus yang Anda presentasikan, Memecah eksekusi menjadi beberapa baris akan menghasilkan hasil yang benar:

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

Karena ada banyak cara untuk menulis kode ini yang seharusnya setara secara fungsional, dan mengingat bahwa hanya beberapa dari mereka yang dikompilasi, kesimpulan saya adalah bahwa ini bukan perilaku yang dimaksudkan dari JDK. Ada bug di suatu tempat dalam evaluasi wildcard yang memiliki super terikat.

Rekomendasi saya adalah mengkompilasi kode yang ada terhadap JDK 8, dan untuk kode yang lebih baru membutuhkan JDK> 8, untuk sepenuhnya menentukan nilai generik.


10
2018-06-28 05:11



Saya membuat MCVE yang berbeda yang menunjukkan perbedaan dalam inferensi jenis:

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 kompilasi berlalu, JDK10 memberi:

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

Jadi sepertinya JDK10 memiliki bug yang bermasalah N untuk List<? super String> di

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

saat menelepon

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

Saya akan merekomendasikan untuk melaporkan masalah ini ke OpenJDK (menghubungkan masalah ini di sini), dan mungkin melaporkan masalah tersebut ke proyek hamcrest.


1
2018-06-29 05:45