Chapter sebelumnya memberi kita parameter koleksi yang luas cakupan bacanya: ? extends menerima semua turunan batas, tetapi menutup rapat penulisannya. Kebutuhan yang berlawanan sama lazimnya: method yang justru mengisi koleksi milik pemanggil. Untuk arah ini Java menyediakan lower-bounded wildcard — List<? super Integer> berarti “daftar bertipe tertentu yang pasti supertype dari Integer”, sehingga List<Integer>, List<Number>, hingga List<Object> semuanya sah masuk. Logikanya terbalik dari extends: menulis Integer ke daftar yang pasti menampung Integer atau lebih umum adalah keputusan yang selalu aman. Pada Chapter ini kita isi tiga jenis daftar dari satu method, melihat sisi baca yang menyempit menjadi Object, lalu merakit kedua wildcard menjadi satu signature lengkap — dan menutup tabel PECS secara penuh.

Method isiSkor berikut mengisi daftar target dengan tiga nilai ujian. Satu signature-nya melayani tiga jenis daftar yang berbeda generasi tipe-nya:

import java.util.ArrayList;
import java.util.List;

public class PengisiNilai {
    // Consumer: daftar hanya ditulis Integer. Bound ? super Integer
    // mengizinkan List<Integer>, List<Number>, dan List<Object>.
    static void isiSkor(List<? super Integer> target) {
        for (int skor = 60; skor <= 80; skor += 10) {
            target.add(skor);   // menulis Integer: sah di semua target
        }
    }

    public static void main(String[] args) {
        List<Integer> skorBulat = new ArrayList<>();
        List<Number> skorAngka = new ArrayList<>();
        List<Object> rak = new ArrayList<>();

        isiSkor(skorBulat);
        isiSkor(skorAngka);
        isiSkor(rak);

        System.out.println("Integer: " + skorBulat);
        System.out.println("Number : " + skorAngka);
        System.out.println("Object : " + rak);

        // Membaca dari List<? super Integer> hanya menjamin Object
        Object contoh = skorAngka.get(0);
        System.out.println("Dibaca : " + contoh.getClass().getSimpleName());
    }
}

Output yang Dihasilkan:

text
Integer: [60, 70, 80]
Number : [60, 70, 80]
Object : [60, 70, 80]
Dibaca : Integer

Mari kita bedah mekanisme di balik keluaran tersebut:

  1. Baris 7 (static void isiSkor(List<? super Integer> target)): batas bawah ? super Integer membatasi type argument menjadi Integer atau supertype-nya. Compiler menoleransi List<Integer>, List<Number>, dan List<Object> — ketiganya mampu menampung Integer tanpa risiko tipe.
  2. Baris 9 (target.add(skor)): penulisan Integer sah di setiap kemungkinan target. Compiler mengganti ? dengan capture yang pasti supertype Integer; menambahkan Integer ke daftar ber-elemen supertype tidak pernah mencemari tipe daftar aslinya.
  3. Baris 15-17 (tiga ArrayList berbeda): perhatikan bahwa ketiganya dibuat dengan type argument berbeda (Integer, Number, Object) tanpa konversi manual saat dikirim ke isiSkor — invariance tidak menghalangi karena wildcard-lah yang menjembatani.
  4. Baris 27-28 (Object contoh = skorAngka.get(0)): sisi baca menyempit. Dari List<? super Integer>, elemen hanya terbaca sebagai Object — satu-satunya tipe yang dijamin dimiliki semua supertype Integer. Baris terakhir mencetak Integer karena itulah tipe runtime objeknya, meskipun tipe statis variabelnya Object.

Mengapa Batasnya Dibalik #

Cara termudah memahami arah super adalah membayangkan kegagalannya bila dibalik. Andaikan isiSkor memakai List<? extends Integer> atau List<Integer> polos, pemanggil dengan List<Number> harus menyalin datanya ke daftar baru — mahal dan tidak perlu. Andaikan pemanggil mengirim List<Double> ke List<? super Integer>, compiler menolak dengan List<Double> cannot be converted to ... karena Double bukan supertype Integer: menulis Integer ke daftar berisi Double akan mencemari tipe daftar tersebut.

Sisi baca ? super memang lebih miskin daripada ? extends. Kompilator menolak pembacaan ber-tipe spesifik dari daftar berbatas bawah — uji di JDK 25 memperlihatkan assignment Number nilai = daftar.get(0) pada parameter List<? super Integer> gagal dengan:

text
CekBacaSuper.java:5: error: incompatible types: CAP#1 cannot be converted to Number
        Number nilai = daftar.get(0);
                                 ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: Integer from capture of ? super Integer

Capture CAP#1 pada pesan tersebut berbunyi extends Object super: Integer — tipe yang tidak diketahui, hanya dipastikan berada di atas Integer dalam hierarki. Compiler tidak dapat menjanjikan elemen bertipe Number karena List<Object> juga sah masuk, dan Object lebih umum daripada Number.

Menggabungkan Producer dan Consumer #

Kekuatan sebenarnya muncul saat kedua wildcard dipakai bersama pada satu signature. Method salin berikut menyalin isi daftar sumber ke daftar tujuan tanpa memedulikan generasi tipe keduanya:

java SalinData.java
import java.util.ArrayList;
import java.util.List;

public class SalinData {
    // Kombinasi PECS: sumber dibaca (? extends T), tujuan ditulis (? super T)
    static <T> void salin(List<? super T> tujuan, List<? extends T> sumber) {
        for (T nilai : sumber) {
            tujuan.add(nilai);
        }
    }

    public static void main(String[] args) {
        List<Integer> bulat = List.of(10, 20, 30);
        List<Number> angka = new ArrayList<>();
        salin(angka, bulat);   // List<Number> menampung isi List<Integer>
        System.out.println("Tujuan: " + angka);

        List<String> nama = List.of("Budi", "Siti");
        List<Object> rak = new ArrayList<>();
        salin(rak, nama);       // List<Object> menampung isi List<String>
        System.out.println("Rak   : " + rak);
    }
}

Output yang Dihasilkan:

text
Tujuan: [10, 20, 30]
Rak   : [Budi, Siti]

Perhatikan dua baris pentingnya: baris 6 menyatakan <T> void salin(List<? super T> tujuan, List<? extends T> sumber) — T dijepit dari sumber (producer) lalu ditulis ke tujuan (consumer), sehingga salin(angka, bulat) mengalirkan List<Integer> ke List<Number> dan salin(rak, nama) mengalirkan List<String> ke List<Object>. Signature ini bukan karangan kita sendiri — method Collections.copy pada pustaka standar memakai bentuk yang sama persis:

java
public static <T> void copy(List<? super T> dest, List<? extends T> src)

PECS Secara Penuh #

Dengan kedua wildcard di tangan, mnemonic PECS kini lengkap terbaca: Producer Extends, Consumer Super. Jadikan tabel berikut daftar periksa saat mendesain parameter koleksi:

Peran parameterArah dataWildcardContoh pada salin
Producer (sumber)dibaca method? extends TList<? extends T> sumber
Consumer (tujuan)ditulis method? super TList<? super T> tujuan
Keduanya (baca dan tulis)dua arahtipe persis Tdaftar kerja internal

Pola yang sama berdenyut di pustaka java.util.function: interface Consumer<T> menampung nilai lewat accept(T) (sisi tulis), sementara Supplier<T> menghasilkan nilai lewat get() (sisi baca). API fungsional Java memakai nama producer/consumer secara eksplisit sehingga peta mental PECS sekali dipelajari terpakai di dua wilayah sekaligus.

Dengan generic class, method, interface, bound, dan kedua wildcard, perangkat bahasa kita kini lengkap. Namun di balik semua itu ada satu mekanisme yang menjelaskan mengapa getClass() mengembalikan class yang sama untuk Kotak<String> dan Kotak<Integer>: compiler menghapus informasi tipe setelah selesai memeriksa. Mekanisme type erasure itulah yang kita bedah pada Chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PengisiNilai.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class PengisiNilai {
    // Consumer: daftar hanya ditulis Integer. Bound ? super Integer
    // mengizinkan List<Integer>, List<Number>, dan List<Object>.
    static void isiSkor(List<? super Integer> target) {
        for (int skor = 60; skor <= 80; skor += 10) {
            target.add(skor);   // menulis Integer: sah di semua target
        }
    }

    public static void main(String[] args) {
        List<Integer> skorBulat = new ArrayList<>();
        List<Number> skorAngka = new ArrayList<>();
        List<Object> rak = new ArrayList<>();

        isiSkor(skorBulat);
        isiSkor(skorAngka);
        isiSkor(rak);

        System.out.println("Integer: " + skorBulat);
        System.out.println("Number : " + skorAngka);
        System.out.println("Object : " + rak);

        // Membaca dari List<? super Integer> hanya menjamin Object
        Object contoh = skorAngka.get(0);
        System.out.println("Dibaca : " + contoh.getClass().getSimpleName());
    }
}

SalinData.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class SalinData {
    // Kombinasi PECS: sumber dibaca (? extends T), tujuan ditulis (? super T)
    static <T> void salin(List<? super T> tujuan, List<? extends T> sumber) {
        for (T nilai : sumber) {
            tujuan.add(nilai);
        }
    }

    public static void main(String[] args) {
        List<Integer> bulat = List.of(10, 20, 30);
        List<Number> angka = new ArrayList<>();
        salin(angka, bulat);   // List<Number> menampung isi List<Integer>
        System.out.println("Tujuan: " + angka);

        List<String> nama = List.of("Budi", "Siti");
        List<Object> rak = new ArrayList<>();
        salin(rak, nama);       // List<Object> menampung isi List<String>
        System.out.println("Rak   : " + rak);
    }
}