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.
Menulis ke Semua Supertype Integer #
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:
Integer: [60, 70, 80]
Number : [60, 70, 80]
Object : [60, 70, 80]
Dibaca : IntegerMari kita bedah mekanisme di balik keluaran tersebut:
- Baris 7 (
static void isiSkor(List<? super Integer> target)): batas bawah? super Integermembatasi type argument menjadiIntegeratau supertype-nya. Compiler menoleransiList<Integer>,List<Number>, danList<Object>— ketiganya mampu menampungIntegertanpa risiko tipe. - Baris 9 (
target.add(skor)): penulisanIntegersah di setiap kemungkinan target. Compiler mengganti?dengan capture yang pasti supertypeInteger; menambahkanIntegerke daftar ber-elemen supertype tidak pernah mencemari tipe daftar aslinya. - Baris 15-17 (tiga
ArrayListberbeda): perhatikan bahwa ketiganya dibuat dengan type argument berbeda (Integer,Number,Object) tanpa konversi manual saat dikirim keisiSkor— invariance tidak menghalangi karena wildcard-lah yang menjembatani. - Baris 27-28 (
Object contoh = skorAngka.get(0)): sisi baca menyempit. DariList<? super Integer>, elemen hanya terbaca sebagaiObject— satu-satunya tipe yang dijamin dimiliki semua supertypeInteger. Baris terakhir mencetakIntegerkarena itulah tipe runtime objeknya, meskipun tipe statis variabelnyaObject.
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:
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 IntegerCapture 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:
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:
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:
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 parameter | Arah data | Wildcard | Contoh pada salin |
|---|---|---|---|
| Producer (sumber) | dibaca method | ? extends T | List<? extends T> sumber |
| Consumer (tujuan) | ditulis method | ? super T | List<? super T> tujuan |
| Keduanya (baca dan tulis) | dua arah | tipe persis T | daftar 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 #
- Oracle: Lower Bounded Wildcards (Java Tutorials)
- Java API: java.util.Collections.copy
- JLS SE25 §4.5.1: Type Arguments and Wildcards
- dev.java: Generics
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PengisiNilai.java
Unduh berkasimport 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 berkasimport 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);
}
}