Kapan Java generics membutuhkan <? memanjang T> alih-alih <T> dan apakah ada kelemahan beralih?

205

Diberikan contoh berikut (menggunakan JUnit dengan pencocokan Hamcrest):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

Ini tidak dikompilasi dengan assertThattanda tangan metode JUnit dari:

public static <T> void assertThat(T actual, Matcher<T> matcher)

Pesan kesalahan kompiler adalah:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

Namun, jika saya mengubah assertThattanda tangan metode ke:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Kemudian kompilasi bekerja.

Jadi tiga pertanyaan:

  1. Mengapa sebenarnya tidak mengkompilasi versi saat ini? Meskipun saya samar-samar memahami masalah kovarian di sini, saya jelas tidak bisa menjelaskannya jika saya harus.
  2. Apakah ada kerugian dalam mengubah assertThatmetode Matcher<? extends T>? Apakah ada kasus lain yang akan rusak jika Anda melakukannya?
  3. Apakah ada gunanya generalisasi assertThatmetode di JUnit? The Matcherkelas tampaknya tidak memerlukannya, karena JUnit memanggil metode pertandingan, yang tidak diketik dengan penampilan generik, dan hanya seperti sebuah upaya untuk memaksa keselamatan jenis yang tidak melakukan apa-apa, karena Matcherhanya akan tidak sebenarnya cocok, dan tes akan gagal terlepas. Tidak ada operasi yang tidak aman yang terlibat (atau begitulah tampaknya).

Untuk referensi, berikut ini adalah implementasi JUnit dari assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}
Yishai
sumber
tautan ini sangat berguna (generik, warisan, dan subtipe): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Jawaban:

145

Pertama - saya harus mengarahkan Anda ke http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - dia melakukan pekerjaan luar biasa.

Ide dasarnya adalah yang Anda gunakan

<T extends SomeClass>

ketika parameter aktual dapat SomeClassatau subtipe apa pun darinya.

Dalam contoh Anda,

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

Anda mengatakan itu expected bisa berisi objek-objek kelas yang mewakili setiap kelas yang mengimplementasikan Serializable. Peta hasil Anda mengatakan itu hanya bisa menampung Dateobjek kelas.

Ketika Anda lulus dalam hasil, Anda pengaturan sedang Tpersis Mapdari Stringke Dateobjek kelas, yang tidak cocok MapdariString apa pun yang ini Serializable.

Satu hal yang perlu diperiksa - apakah Anda yakin mau Class<Date>dan tidak Date? Peta Stringuntuk Class<Date>tidak terdengar sangat berguna secara umum (yang dapat disimpan hanyalahDate.class sebagai nilai daripada contoh Date)

Adapun generalisasi assertThat, idenya adalah bahwa metode ini dapat memastikan bahwa Matcheryang sesuai dengan jenis hasil diteruskan.

Scott Stanchfield
sumber
Dalam hal ini, ya saya ingin peta kelas. Contoh yang saya berikan dibuat untuk menggunakan kelas JDK standar daripada kelas kustom saya, tetapi dalam kasus ini kelas tersebut sebenarnya dipakai melalui refleksi dan digunakan berdasarkan kunci. (Aplikasi terdistribusi di mana klien tidak memiliki kelas server yang tersedia, hanya kunci kelas mana yang digunakan untuk melakukan pekerjaan sisi server).
Yishai
6
Saya kira di mana otak saya macet adalah mengapa Peta yang berisi kelas tipe Tanggal tidak hanya cocok dengan tipe Peta yang berisi kelas tipe Serializable. Tentu kelas tipe Serializable bisa menjadi kelas lain juga, tetapi tentu saja termasuk tipe Date.
Yishai
Pada pernyataan bahwa memastikan para pemain melakukan untuk Anda, metode matcher.matches () tidak peduli, jadi karena T tidak pernah digunakan, mengapa melibatkannya? (jenis metode pengembalian tidak berlaku)
Yishai
Ahhh - itulah yang saya dapatkan karena tidak membaca def dari pernyataan itu cukup dekat. Tampak seperti itu hanya untuk memastikan bahwa Matcher pas dilewatkan di ...
Scott Stanchfield
28

Terima kasih kepada semua orang yang menjawab pertanyaan, itu sangat membantu mengklarifikasi hal-hal untuk saya. Pada akhirnya jawaban Scott Stanchfield paling mendekati bagaimana saya akhirnya memahaminya, tetapi karena saya tidak memahaminya ketika dia pertama kali menulisnya, saya mencoba untuk menyatakan kembali masalahnya sehingga mudah-mudahan orang lain akan mendapat manfaat.

Saya akan menyatakan kembali pertanyaan dalam hal Daftar, karena hanya memiliki satu parameter umum dan itu akan membuatnya lebih mudah untuk dipahami.

Tujuan dari kelas parametrized (seperti Daftar <Date>atau Peta <K, V>seperti dalam contoh) adalah untuk memaksa orang yang tertekan dan memiliki jaminan kompiler bahwa ini aman (tidak ada pengecualian runtime).

Pertimbangkan kasus Daftar. Inti dari pertanyaan saya adalah mengapa metode yang menggunakan tipe T dan Daftar tidak akan menerima Daftar sesuatu yang lebih jauh dari rantai warisan daripada T. Pertimbangkan contoh yang dibuat-buat ini:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

Ini tidak akan dikompilasi, karena parameter daftar adalah daftar tanggal, bukan daftar string. Generik tidak akan sangat berguna jika ini dikompilasi.

Hal yang sama berlaku untuk Peta. <String, Class<? extends Serializable>>Ini bukan hal yang sama dengan Peta <String, Class<java.util.Date>>. Mereka bukan kovarian, jadi jika saya ingin mengambil nilai dari peta yang berisi kelas tanggal dan memasukkannya ke dalam peta yang mengandung elemen serializable, itu bagus, tapi metode tanda tangan yang mengatakan:

private <T> void genericAdd(T value, List<T> list)

Ingin dapat melakukan keduanya:

T x = list.get(0);

dan

list.add(value);

Dalam hal ini, meskipun metode junit tidak benar-benar peduli tentang hal-hal ini, tanda tangan metode memerlukan kovarians, yang tidak didapat, oleh karena itu tidak dikompilasi.

Pada pertanyaan kedua,

Matcher<? extends T>

Akan memiliki kerugian untuk benar-benar menerima apa pun ketika T adalah Obyek, yang bukan maksud API. Maksudnya adalah untuk memastikan secara statis bahwa pencocokan cocok dengan objek yang sebenarnya, dan tidak ada cara untuk mengecualikan Objek dari perhitungan itu.

Jawaban untuk pertanyaan ketiga adalah bahwa tidak ada yang akan hilang, dalam hal fungsionalitas yang tidak dicentang (tidak akan ada typecasting yang tidak aman dalam JUnit API jika metode ini tidak digeneralisasi), tetapi mereka mencoba untuk menyelesaikan sesuatu yang lain - secara statis memastikan bahwa dua parameter cenderung cocok.

EDIT (setelah kontemplasi dan pengalaman lebih lanjut):

Salah satu masalah besar dengan tanda tangan metode assertThat adalah upaya untuk menyamakan variabel T dengan parameter generik dari T. Itu tidak berfungsi, karena mereka tidak kovarian. Jadi misalnya Anda mungkin memiliki T yang merupakan List<String>tetapi kemudian melewati kecocokan yang berhasil dikompilasi oleh kompiler Matcher<ArrayList<T>>. Sekarang jika itu bukan parameter tipe, semuanya akan baik-baik saja, karena List dan ArrayList adalah kovarian, tetapi karena Generics, sejauh menyangkut kompiler memerlukan ArrayList, ia tidak dapat mentolerir Daftar karena alasan yang saya harap jelas. dari atas.

Yishai
sumber
Saya masih belum mengerti mengapa saya tidak bisa menolak. Mengapa saya tidak bisa mengubah daftar tanggal menjadi daftar serializable?
Thomas Ahle
@ Thomas, karena referensi yang menganggap itu adalah daftar Tanggal akan mengalami kesalahan casting ketika mereka menemukan Strings atau serializables lainnya.
Yishai
Saya mengerti, tetapi bagaimana jika saya entah bagaimana menyingkirkan referensi lama, seolah-olah saya mengembalikan List<Date>dari metode dengan tipe List<Object>? Itu harus aman kan, bahkan jika tidak diizinkan oleh java.
Thomas Ahle
14

Intinya adalah:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

Anda dapat melihat referensi Kelas c1 dapat berisi contoh Panjang (karena objek yang mendasari pada waktu tertentu bisa saja List<Long>), tetapi jelas tidak dapat dilemparkan ke Tanggal karena tidak ada jaminan bahwa kelas "tidak dikenal" adalah Tanggal. Ini bukan typsesafe, jadi kompiler melarangnya.

Namun, jika kami memperkenalkan beberapa objek lain, misalnya Daftar (dalam contoh Anda objek ini adalah Pencocokan), maka berikut ini menjadi benar:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Namun, jika jenis Daftar menjadi? memanjang T bukannya T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Saya pikir dengan mengubah Matcher<T> to Matcher<? extends T>, Anda pada dasarnya memperkenalkan skenario yang mirip dengan menetapkan l1 = l2;

Masih sangat membingungkan memiliki wildcard yang bersarang, tetapi mudah-mudahan itu masuk akal mengapa hal ini membantu untuk memahami obat generik dengan melihat bagaimana Anda dapat menetapkan referensi umum satu sama lain. Ini juga lebih membingungkan karena kompiler sedang menyimpulkan jenis T ketika Anda membuat panggilan fungsi (Anda tidak secara eksplisit mengatakan itu adalah T).

GreenieMeanie
sumber
9

Alasan kode asli Anda tidak kompilasi adalah bahwa <? extends Serializable>tidak tidak berarti, "setiap kelas yang meluas Serializable," tapi "beberapa diketahui tetapi khusus kelas yang memanjang Serializable."

Sebagai contoh, diberi kode seperti yang tertulis, itu benar-benar berlaku untuk menetapkan new TreeMap<String, Long.class>()>ke expected. Jika kompiler mengijinkan kode untuk dikompilasi, assertThat()kemungkinan akan rusak karena akan mengharapkan Dateobjek, bukan Longobjek yang ditemukan di peta.

erickson
sumber
1
Saya tidak cukup mengikuti - ketika Anda mengatakan "tidak berarti ... tapi ...": apa bedanya? (seperti, apa yang akan menjadi contoh dari "dikenal tapi tidak spesifik" kelas yang cocok mantan definisi tetapi tidak yang terakhir?)
poundifdef
Ya, itu agak canggung; tidak yakin bagaimana mengekspresikannya dengan lebih baik ... apakah lebih masuk akal untuk mengatakan, "'?' adalah tipe yang tidak diketahui, bukan tipe yang cocok dengan apa pun? "
erickson
1
Apa yang bisa membantu menjelaskannya adalah bahwa untuk "kelas apa pun yang meluas Serializable" Anda bisa menggunakan<Serializable>
c0der
8

Salah satu cara bagi saya untuk memahami wildcard adalah dengan berpikir bahwa wildcard tidak menentukan jenis objek yang mungkin diberikan referensi generik dapat "dimiliki", tetapi jenis referensi umum lain yang kompatibel dengannya (ini mungkin terdengar membingungkan ...) Dengan demikian, jawaban pertama sangat menyesatkan dalam kata-katanya.

Dengan kata lain, List<? extends Serializable>berarti Anda dapat menetapkan referensi itu ke Daftar lain di mana jenisnya adalah beberapa jenis yang tidak diketahui yang merupakan atau subkelas Serializable. JANGAN memikirkannya dalam hal A SINGLE LIST yang dapat menampung subclass dari Serializable (karena itu adalah semantik yang salah dan mengarah pada kesalahpahaman Generik).

GreenieMeanie
sumber
Itu memang membantu, tetapi "mungkin terdengar membingungkan" diganti dengan "terdengar membingungkan." Sebagai tindak lanjut, jadi mengapa, menurut penjelasan ini, apakah metode dengan Matcher <? extends T>dikompilasi?
Yishai
jika kita mendefinisikan sebagai Daftar <Serializable>, apakah ia melakukan hal yang sama, benar? Saya sedang berbicara tentang para ke-2 Anda. yaitu polimorfisme akan menanganinya?
Supun Wijerathne
3

Saya tahu ini adalah pertanyaan lama tetapi saya ingin membagikan contoh yang menurut saya menjelaskan wildcard terbatas. java.util.Collectionsmenawarkan metode ini:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

Jika kita memiliki Daftar T, Daftar dapat, tentu saja, berisi contoh jenis yang diperluas T. Jika Daftar berisi Hewan, Daftar dapat berisi Anjing dan Kucing (keduanya Hewan). Anjing memiliki properti "woofVolume" dan Kucing memiliki properti "meowVolume." Meskipun kami mungkin ingin menyortir berdasarkan properti-properti ini khusus untuk subclass T, bagaimana kita bisa mengharapkan metode ini untuk melakukan itu? Keterbatasan Pembanding adalah ia hanya dapat membandingkan dua hal dari hanya satu jenis ( T). Jadi, hanya membutuhkan Comparator<T>akan membuat metode ini dapat digunakan. Tapi, pencipta metode ini mengakui bahwa jika sesuatu adalah T, maka itu juga merupakan contoh dari superclasses T. Oleh karena itu, ia memungkinkan kami untuk menggunakan Pembanding Tatau superclass T, yaitu ? super T.

Lucas Ross
sumber
1

bagaimana jika Anda menggunakan

Map<String, ? extends Class<? extends Serializable>> expected = null;
berita baru
sumber
Ya, inilah yang mendorong jawaban saya di atas.
GreenieMeanie
Tidak, itu tidak membantu situasi, setidaknya cara saya mencoba.
Yishai