Wildcard dan PECS

Menggunakan ? extends dan ? super dengan aturan producer extends, consumer super.

Daftar isi

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.

java Contoh: penugasan antar tipe List yang ditolak
// 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.

java UjiProducer.java
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;
    }
}
text Keluaran
Jumlah skor  : 247.0
Jumlah berat : 3.25

Arah 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.

java Contoh: penambahan elemen ke List<? extends Number>
// 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)

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.

java UjiConsumer.java
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);
    }
}
text Keluaran
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.

java UjiSalin.java
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);
        }
    }
}
text Keluaran
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.

Arah data pada producer dengan extends dan consumer dengan superproducer: ? extendsList<? extends Number>dibaca (get)consumer: ? superditulis (add)List<? super Integer>nilai mengalir keluar dari producer, dan mengalir masuk ke consumer
Arah data pada producer dengan ? extends dan consumer dengan ? super

Kesalahan umum#

  • Mengira List<Object> mampu menampung List<String> lewat penugasan biasa. Tipe generic bersifat invarian, sehingga penugasan itu ditolak. Gunakan List<? super String> bila daftar hanya ditulisi, atau List<? extends Object> bila hanya dibaca.
  • Menambahkan elemen ke List<? extends T> lalu bingung dengan pesan CAP#1. Kompilator menolaknya karena tipe asli daftar tidak diketahui pasti. Pindahkan penambahannya ke parameter bertipe ? super.
  • Memilih ? extends untuk parameter yang justru ditulisi, misalnya daftar tujuan. Setiap pemanggilan add akan gagal dikompilasi. Terapkan PECS, yaitu ? super untuk consumer.
  • Menganggap ? extends Number berarti 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 ke List<Object> meskipun String turunan Object.
  • ? extends T melonggarkan sumber data yang dibaca, dengan elemen terbaca dijamin bertipe T.
  • ? super T melonggarkan tujuan data yang ditulisi, dengan elemen terbaca hanya dijamin bertipe Object.
  • PECS merangkum pilihannya, yaitu producer extends dan consumer super.

Referensi#