Alasan untuk ini didasarkan pada bagaimana Java mengimplementasikan obat generik.
Contoh Array
Dengan array, Anda dapat melakukan ini (array bersifat kovarian)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Tetapi, apa yang akan terjadi jika Anda mencoba melakukan ini?
myNumber[0] = 3.14; //attempt of heap pollution
Baris terakhir ini akan mengkompilasi dengan baik, tetapi jika Anda menjalankan kode ini, Anda bisa mendapatkan ArrayStoreException
. Karena Anda mencoba memasukkan dobel ke dalam array integer (terlepas dari diakses melalui referensi nomor).
Ini berarti bahwa Anda dapat menipu kompilator, tetapi Anda tidak dapat menipu sistem tipe runtime. Dan ini terjadi karena array adalah apa yang kita sebut tipe yang dapat diverifikasi . Ini berarti bahwa pada saat runtime Java tahu bahwa array ini sebenarnya dipakai sebagai array integer yang kebetulan diakses melalui referensi tipe Number[]
.
Jadi, seperti yang Anda lihat, satu hal adalah jenis objek yang sebenarnya, dan satu hal lagi adalah jenis referensi yang Anda gunakan untuk mengaksesnya, bukan?
Masalah dengan Java Generics
Sekarang, masalah dengan tipe generik Java adalah bahwa informasi tipe dibuang oleh kompiler dan tidak tersedia pada saat run time. Proses ini disebut tipe erasure . Ada alasan bagus untuk mengimplementasikan obat generik seperti ini di Jawa, tetapi itu adalah cerita yang panjang, dan itu harus dilakukan, antara lain, dengan kompatibilitas biner dengan kode yang sudah ada sebelumnya (lihat Bagaimana kami mendapatkan obat generik yang kami miliki ).
Tetapi poin penting di sini adalah karena, pada saat runtime tidak ada informasi jenis, tidak ada cara untuk memastikan bahwa kami tidak melakukan polusi timbunan.
Misalnya,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Jika kompiler Java tidak menghentikan Anda dari melakukan ini, sistem tipe runtime tidak dapat menghentikan Anda juga, karena tidak ada cara, pada saat runtime, untuk menentukan bahwa daftar ini seharusnya menjadi daftar bilangan bulat saja. Java runtime akan membiarkan Anda memasukkan apa pun yang Anda inginkan ke dalam daftar ini, padahal seharusnya hanya berisi bilangan bulat, karena ketika dibuat, itu dinyatakan sebagai daftar bilangan bulat.
Dengan demikian, para perancang Java memastikan bahwa Anda tidak dapat membodohi kompiler. Jika Anda tidak dapat membodohi kompiler (seperti yang dapat kita lakukan dengan array), Anda juga tidak dapat mengelabui sistem tipe runtime.
Karena itu, kami mengatakan bahwa tipe generik tidak dapat diverifikasi .
Jelas, ini akan menghambat polimorfisme. Perhatikan contoh berikut:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Sekarang Anda bisa menggunakannya seperti ini:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
Tetapi jika Anda mencoba menerapkan kode yang sama dengan koleksi umum, Anda tidak akan berhasil:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Anda akan mendapatkan eril kompiler jika Anda mencoba ...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
Solusinya adalah belajar menggunakan dua fitur kuat dari generik Java yang dikenal sebagai kovarians dan contravariance.
Kovarian
Dengan kovarian Anda dapat membaca item dari suatu struktur, tetapi Anda tidak dapat menulis apa pun di dalamnya. Semua ini adalah deklarasi yang valid.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
Dan Anda dapat membaca dari myNums
:
Number n = myNums.get(0);
Karena Anda dapat yakin bahwa apa pun yang ada dalam daftar aktual, daftar itu dapat disebarluaskan ke suatu Angka (setelah semua hal yang meluas Angka adalah suatu Angka, bukan?)
Namun, Anda tidak diizinkan memasukkan apa pun ke dalam struktur kovarian.
myNumst.add(45L); //compiler error
Ini tidak akan diizinkan, karena Java tidak dapat menjamin apa jenis objek yang sebenarnya dalam struktur generik. Itu bisa berupa apa saja yang meluas Bilangan, tetapi kompiler tidak dapat memastikan. Jadi Anda bisa membaca, tetapi tidak menulis.
Contravariance
Dengan contravariance Anda bisa melakukan yang sebaliknya. Anda dapat meletakkan berbagai hal dalam struktur umum, tetapi Anda tidak dapat membacanya.
List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
Dalam hal ini, sifat sebenarnya dari objek adalah Daftar Objek, dan melalui contravariance, Anda dapat memasukkan Angka ke dalamnya, pada dasarnya karena semua angka memiliki Objek sebagai leluhur bersama mereka. Dengan demikian, semua Bilangan adalah objek, dan karenanya ini valid.
Namun, Anda tidak dapat dengan aman membaca apa pun dari struktur contravarian ini dengan asumsi Anda akan mendapatkan nomor.
Number myNum = myNums.get(0); //compiler-error
Seperti yang Anda lihat, jika kompiler mengizinkan Anda untuk menulis baris ini, Anda akan mendapatkan ClassCastException saat runtime.
Prinsip Get / Put
Dengan demikian, gunakan kovarians ketika Anda hanya bermaksud untuk mengambil nilai-nilai generik dari suatu struktur, gunakan contravariance ketika Anda hanya bermaksud untuk memasukkan nilai-nilai generik ke dalam struktur dan menggunakan tipe generik yang tepat ketika Anda ingin melakukan keduanya.
Contoh terbaik yang saya miliki adalah yang berikut ini yang menyalin segala jenis angka dari satu daftar ke daftar lain. Itu hanya mendapatkan item dari sumber, dan itu hanya menempatkan item di target.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Berkat kekuatan kovarians dan contravariance, ini berfungsi untuk kasus seperti ini:
List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);