Wildcard melonggarkan pencocokan tipe generic ketika argumen tipenya memang belum pasti, misalnya satu metode yang harus menerima List<Integer> sekaligus List<Double>. Topik ini menjelaskan mengapa List<String> tidak boleh ditugaskan ke List<Object>, cara membaca dengan ? extends, cara menulis dengan ? super, serta aturan PECS yang merangkum keduanya. Setelah menyelesaikannya, parameter metode generic dapat dirancang sesuai arah data yang melewatinya.
Mengapa List bukan List
Pada array, String[] boleh ditugaskan ke Object[] karena array bersifat kovarian. Tipe generic memilih jalan berbeda, yaitu List<String> bukan turunan List<Object> meskipun String turunan Object. Sifat ini disebut invarian (invariant) atas argumen tipe, dan alasan keputusannya terlihat dari penugasan berikut.
// tidak-dijalankan
List<String> daftarKata = new ArrayList<>();
List<Object> daftarObjek = daftarKata;
// error: incompatible types: List<String> cannot be converted to List<Object>Bila penugasan itu diizinkan, daftarObjek.add(42) akan menambahkan Integer ke dalam daftar yang sebenarnya bertipe String. Pembacaan berikutnya lewat daftarKata lalu memicu ClassCastException di tempat yang jauh dari penyebabnya. Kompilator mencegah kekacauan tersebut dengan menolak penugasan sejak awal, dan wildcard-lah yang menyediakan pelonggaran yang tetap aman.
Perbandingan dengan array menunjukkan taruhan yang berbeda. Pada array, kekeliruan penugasan seperti di atas tetap terkompilasi, lalu baru meledak sebagai ArrayStoreException saat elemennya ditulisi. Generics memindahkan titik kegagalan itu ke kompilasi, sehingga program yang lolos kompilasi tidak membawa jenis kejutan tersebut. Konsekuensinya, penugasan antar tipe generic memang lebih ketat, dan pelonggarannya harus diminta secara sadar lewat wildcard yang dibahas di bagian berikutnya.
Membaca dengan ? extends#
Wildcard ? extends T menyatakan T sendiri atau salah satu turunannya, tanpa menentukan tipe pastinya. Variabel bertipe List<? extends Number> menerima List<Integer>, List<Double>, maupun List<Number>. Elemen yang terbaca dijamin bertipe Number, sehingga operasi membaca menjadi aman. Metode berikut menjumlahkan isi daftar angka tanpa peduli tipe aslinya.
import java.util.List;
public class UjiProducer {
public static void main(String[] args) {
List<Integer> skor = List.of(80, 92, 75);
List<Double> berat = List.of(2.5, 0.75);
System.out.println("Jumlah skor : " + jumlah(skor));
System.out.println("Jumlah berat : " + jumlah(berat));
}
// daftar dipakai sebagai producer: nilainya dibaca saja
static double jumlah(List<? extends Number> daftar) {
double total = 0;
for (Number n : daftar) {
total += n.doubleValue();
}
return total;
}
}Jumlah skor : 247.0
Jumlah berat : 3.25Arah sebaliknya tidak diizinkan. Kompilator tidak mengetahui tipe asli daftar tersebut, bisa Integer atau Double, sehingga penambahan elemen apa pun berpotensi merusak isinya. Pemanggilan daftar.add(0) atas List<? extends Number> ditolak dengan pesan int cannot be converted to CAP#1, yaitu nama sementara yang dipakai kompilator untuk tipe yang belum diketahui pastinya. Karena itu ? extends cocok untuk sumber data yang hanya dibaca.
// tidak-dijalankan
static void tambahNol(List<? extends Number> daftar) {
daftar.add(0);
}
// error: no suitable method found for add(int)
// (argument mismatch; int cannot be converted to CAP#1)Menulis dengan ? super#
Wildcard ? super T menyatakan T sendiri atau salah satu leluhurnya. Variabel bertipe List<? super Integer> menerima List<Integer>, List<Number>, maupun List<Object>. Penambahan Integer dijamin aman pada ketiganya, karena Integer muat di semua tipe itu. Metode berikut mengisi tiga daftar berbeda melalui satu metode saja.
Nilai yang ditambahkan harus bertipe Integer atau tipenya berada di bawah Integer. Yang ditolak hanyalah tipe di luar rantai itu, misalnya Double, karena meskipun List<Number> sungguh menerima Double, kompilator tidak tahu daftarnya benar-benar List<Number>. Batasan ? super Integer karena itu tidak berarti daftarnya berisi Integer saja, melainkan daftar yang dijamin aman menerima Integer.
import java.util.ArrayList;
import java.util.List;
public class UjiConsumer {
public static void main(String[] args) {
List<Integer> targetInt = new ArrayList<>();
List<Number> targetNumber = new ArrayList<>();
List<Object> targetObject = new ArrayList<>();
isiNomor(targetInt);
isiNomor(targetNumber);
isiNomor(targetObject);
System.out.println("List<Integer> : " + targetInt);
System.out.println("List<Number> : " + targetNumber);
System.out.println("List<Object> : " + targetObject);
}
// daftar dipakai sebagai consumer: nilainya ditulis saja
static void isiNomor(List<? super Integer> daftar) {
daftar.add(1);
daftar.add(2);
daftar.add(3);
}
}List<Integer> : [1, 2, 3]
List<Number> : [1, 2, 3]
List<Object> : [1, 2, 3]Pembacaan menjadi batasnya. Elemen yang diambil dari List<? super Integer> hanya dijamin bertipe Object, karena tipe aslinya bisa Integer, Number, atau Object itu sendiri. Bila tipe yang lebih spesifik diperlukan, nilainya harus dipaksa dan keamanannya kembali menjadi tanggung jawab penulis kodenya. Karena itu ? super cocok untuk tujuan data yang hanya ditulisi.
Aturan PECS#
PECS adalah singkatan dari producer extends, consumer super, yakni tipe yang memproduksi (producer) nilai diberi ? extends, sedangkan tipe yang mengonsumsi (consumer) nilai diberi ? super. Singkatan ini dipopulerkan oleh Joshua Bloch melalui buku Effective Java dan kini menjadi pedoman umum. Pada metode salin berikut, sumber berperan sebagai producer dan tujuan berperan sebagai consumer.
import java.util.ArrayList;
import java.util.List;
public class UjiSalin {
public static void main(String[] args) {
List<Integer> sumber = List.of(11, 22, 33);
List<Number> tujuan = new ArrayList<>();
salin(sumber, tujuan);
System.out.println("Isi tujuan : " + tujuan);
}
// sumber diproduksi (? extends), tujuan dikonsumsi (? super)
static <T> void salin(List<? extends T> sumber, List<? super T> tujuan) {
for (T item : sumber) {
tujuan.add(item);
}
}
}Isi tujuan : [11, 22, 33]Parameter sumber bertipe List<? extends T> karena nilainya hanya dibaca, sedangkan parameter tujuan bertipe List<? super T> karena nilainya hanya ditambah. Pemanggilan salin(sumber, tujuan) menyimpulkan T sebagai Integer, lalu memindahkan setiap elemen tanpa cast. Bentuk dua wildcard sekaligus seperti ini dipakai pustaka standar, misalnya pada Collections.copy. Diagram berikut merangkum arah datanya.
Kesalahan umum#
- Mengira
List<Object>mampu menampungList<String>lewat penugasan biasa. Tipe generic bersifat invarian, sehingga penugasan itu ditolak. GunakanList<? super String>bila daftar hanya ditulisi, atauList<? extends Object>bila hanya dibaca. - Menambahkan elemen ke
List<? extends T>lalu bingung dengan pesanCAP#1. Kompilator menolaknya karena tipe asli daftar tidak diketahui pasti. Pindahkan penambahannya ke parameter bertipe? super. - Memilih
? extendsuntuk parameter yang justru ditulisi, misalnya daftar tujuan. Setiap pemanggilanaddakan gagal dikompilasi. Terapkan PECS, yaitu? superuntuk consumer. - Menganggap
? extends Numberberarti daftar berisi Number sehingga bebas diisi Number. Wildcard menyatakan rentang tipe yang mungkin, bukan perintah konversi, dan penambahannya tetap ditolak.
Ringkasan#
- Tipe generic bersifat invarian, sehingga
List<String>tidak boleh ditugaskan keList<Object>meskipun String turunan Object. ? extends Tmelonggarkan sumber data yang dibaca, dengan elemen terbaca dijamin bertipe T.? super Tmelonggarkan tujuan data yang ditulisi, dengan elemen terbaca hanya dijamin bertipe Object.- PECS merangkum pilihannya, yaitu producer extends dan consumer super.
Referensi#
- Wildcards — Oracle — panduan resmi wildcard beserta contoh penggunaannya pada parameter metode.
- §4.5.1 Type Arguments and Wildcards — Oracle — spesifikasi sintaks argumen tipe dan wildcard.