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 assertThat
tanda 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 assertThat
tanda tangan metode ke:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Kemudian kompilasi bekerja.
Jadi tiga pertanyaan:
- Mengapa sebenarnya tidak mengkompilasi versi saat ini? Meskipun saya samar-samar memahami masalah kovarian di sini, saya jelas tidak bisa menjelaskannya jika saya harus.
- Apakah ada kerugian dalam mengubah
assertThat
metodeMatcher<? extends T>
? Apakah ada kasus lain yang akan rusak jika Anda melakukannya? - Apakah ada gunanya generalisasi
assertThat
metode di JUnit? TheMatcher
kelas 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, karenaMatcher
hanya 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());
}
}
Jawaban:
Pertama - saya harus mengarahkan Anda ke http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - dia melakukan pekerjaan luar biasa.
Ide dasarnya adalah yang Anda gunakan
ketika parameter aktual dapat
SomeClass
atau subtipe apa pun darinya.Dalam contoh Anda,
Anda mengatakan itu
expected
bisa berisi objek-objek kelas yang mewakili setiap kelas yang mengimplementasikanSerializable
. Peta hasil Anda mengatakan itu hanya bisa menampungDate
objek kelas.Ketika Anda lulus dalam hasil, Anda pengaturan sedang
T
persisMap
dariString
keDate
objek kelas, yang tidak cocokMap
dariString
apa pun yang iniSerializable
.Satu hal yang perlu diperiksa - apakah Anda yakin mau
Class<Date>
dan tidakDate
? PetaString
untukClass<Date>
tidak terdengar sangat berguna secara umum (yang dapat disimpan hanyalahDate.class
sebagai nilai daripada contohDate
)Adapun generalisasi
assertThat
, idenya adalah bahwa metode ini dapat memastikan bahwaMatcher
yang sesuai dengan jenis hasil diteruskan.sumber
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:
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:Ingin dapat melakukan keduanya:
dan
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,
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 kompilerMatcher<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.sumber
List<Date>
dari metode dengan tipeList<Object>
? Itu harus aman kan, bahkan jika tidak diizinkan oleh java.Intinya adalah:
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:
... Namun, jika jenis Daftar menjadi? memanjang T bukannya T ....
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).
sumber
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>()>
keexpected
. Jika kompiler mengijinkan kode untuk dikompilasi,assertThat()
kemungkinan akan rusak karena akan mengharapkanDate
objek, bukanLong
objek yang ditemukan di peta.sumber
<Serializable>
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).sumber
<? extends T>
dikompilasi?Saya tahu ini adalah pertanyaan lama tetapi saya ingin membagikan contoh yang menurut saya menjelaskan wildcard terbatas.
java.util.Collections
menawarkan metode ini:Jika kita memiliki Daftar
T
, Daftar dapat, tentu saja, berisi contoh jenis yang diperluasT
. 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 subclassT
, 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 membutuhkanComparator<T>
akan membuat metode ini dapat digunakan. Tapi, pencipta metode ini mengakui bahwa jika sesuatu adalahT
, maka itu juga merupakan contoh dari superclassesT
. Oleh karena itu, ia memungkinkan kami untuk menggunakan PembandingT
atau superclassT
, yaitu? super T
.sumber
bagaimana jika Anda menggunakan
sumber