Kita kini menguasai seluruh perangkat: generic class, method, interface, bound, kedua wildcard, erasure, dan inferensi. Perangkat yang lengkap justru menghadirkan pertanyaan desain: kapan memparametrisasi, seberapa jauh membatasi, dan bentuk signature seperti apa yang enak dipakai pemanggil? API generik yang baik membuat kesalahan tipe mustahil dikompilasi, sementara API generik yang berlebihan memaksa pemanggil menulis tanda kurung sudut bertingkat hanya untuk operasi sederhana. Pada Chapter penutup Section ini kita rangkai lima prinsip perancangan, lalu praktikkan satu per satu lewat studi kasus: membangun class Tumpukan<E> — struktur stack LIFO — dari signature paling kaku menuju bentang API yang luas namun tetap type-safe.
Prinsip 1 Mulai dari yang Konkret #
Godaan desain generics adalah memparametrisasi terlalu dini. Sebelum ada bukti kebutuhan, class konkret dengan tipe tetap lebih mudah ditulis, diuji, dan dibaca; <E> dapat ditambahkan kemudian karena compiler akan menuntun setiap titik yang perlu digeneralkan. Panduan praktisnya: parametrisasi saat dua hingga tiga tipe berbeda terbukti membutuhkan struktur yang sama persis — seperti ArrayList<String> dan ArrayList<Integer> membutuhkan daftar.
Batas atas juga perlu dihormati: type parameter kedua memang lazim (Map<K, V>), tetapi lima type parameter membuat pemanggil kesulitan mengingat makna tiap posisi. Bila struktur memang kompleks, pecah menjadi class kecil bernama spesifik — Koordinat<Lintang, Bujur> membaca jauh lebih jelas daripada Tuple5<A, B, C, D, E>.
Prinsip 2 Gunakan Bound Hanya Saat Dibutuhkan #
Bound bukan aksesori; setiap extends pada type parameter adalah kontrak yang membatasi pemanggil. Tambahkan bound hanya bila body class atau method benar-benar memanggil member milik tipe batas — <T extends Number> karena doubleValue() dipanggil, <T extends Comparable<T>> karena compareTo() dipanggil. Tanpa kebutuhan tersebut, bound hanya menutup pemanggilan sah tanpa manfaat. Prinsip ini merupakan kebalikan dari Prinsip 1: mulai longgar, batasi hanya dengan alasan yang tertulis di dalam kode.
Studi Kasus Tumpukan E #
Dengan dua prinsip pertama, Tumpukan<E> lahir sebagai class sederhana dengan kontrak yang terdokumentasi di Javadoc — invariant perilaku dituliskan eksplisit, bukan ditebak pemanggil:
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.NoSuchElementException;
public class Tumpukan<E> {
private final Deque<E> data = new ArrayDeque<>();
/**
* Menambahkan satu elemen ke puncak tumpukan.
* Elemen null tidak diterima.
*/
public void dorong(E elemen) {
if (elemen == null) {
throw new NullPointerException("elemen tidak boleh null");
}
data.push(elemen);
}
/**
* Menambahkan seluruh elemen sumber ke puncak tumpukan, berurutan
* dari elemen pertama hingga terakhir. Sumber hanya dibaca (producer),
* sehingga Iterable<? extends E> diterima.
*/
public void dorongSemua(Iterable<? extends E> sumber) {
for (E elemen : sumber) {
dorong(elemen);
}
}
/**
* Mengambil dan menghapus elemen puncak tumpukan.
* Melempar NoSuchElementException bila tumpukan kosong.
*/
public E ambil() {
if (data.isEmpty()) {
throw new NoSuchElementException("tumpukan kosong");
}
return data.pop();
}
/**
* Memindahkan seluruh isi tumpukan ke koleksi tujuan, mulai dari
* puncak hingga dasar. Tujuan hanya ditulis (consumer), sehingga
* Collection<? super E> diterima. Tumpukan menjadi kosong setelah
* pemanggilan berhasil.
*/
public void ambilSemua(Collection<? super E> tujuan) {
while (!data.isEmpty()) {
tujuan.add(ambil());
}
}
/**
* Mengintip elemen puncak tanpa menghapusnya.
* Melempar NoSuchElementException bila tumpukan kosong.
*/
public E intip() {
if (data.isEmpty()) {
throw new NoSuchElementException("tumpukan kosong");
}
return data.peek();
}
/** Banyaknya elemen saat ini. */
public int ukuran() {
return data.size();
}
/** Benar bila tumpukan tidak memiliki elemen. */
public boolean kosong() {
return data.isEmpty();
}
}Perhatikan bahwa signature dasarnya (dorong(E), ambil(), intip()) memakai E polos tanpa wildcard — parameter menerima tepat satu nilai dan tipe kembalian memberi tepat satu nilai. Wildcard belum dibutuhkan di sini, dan menuliskannya justru menurunkan kejelasan.
Prinsip 3 Terapkan PECS pada Parameter Koleksi #
Kebutuhan wildcard muncul begitu API bertumbuh menjadi operasi koleksi. Kandidat pertama: method untuk mendorong sekaligus banyak elemen. Signature naif dorongSemua(Iterable<E> sumber) terasa wajar, tetapi menolak List<Integer> pada Tumpukan<Number> — invariance yang kita pahami sejak Chapter Wildcards:
// Tahap awal: menolak Iterable<Integer> untuk Tumpukan<Number>
public void dorongSemua(Iterable<E> sumber)
// Tahap PECS: sumber adalah producer — hanya dibaca
public void dorongSemua(Iterable<? extends E> sumber)
// Tahap awal: hanya menerima Collection<Number> persis
public void ambilSemua(Collection<E> tujuan)
// Tahap PECS: tujuan adalah consumer — hanya ditulis
public void ambilSemua(Collection<? super E> tujuan)Versi final Tumpukan.java memakai signature tahap PECS untuk kedua method. Program berikut memerankan seluruh API — termasuk tumpukan kosong yang memicu invariant terdokumentasi:
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class AplikasiTumpukan {
public static void main(String[] args) {
Tumpukan<Number> kotak = new Tumpukan<>();
// Producer: Iterable<Integer> masuk ke Tumpukan<Number>
kotak.dorongSemua(List.of(10, 20, 30));
System.out.println("Ukuran: " + kotak.ukuran());
// Tipe kembalian konkret: E = Number, bukan wildcard
Number puncak = kotak.intip();
System.out.println("Puncak: " + puncak);
// Consumer: isi mengalir ke List<Object>
List<Object> hasil = new ArrayList<>();
kotak.ambilSemua(hasil);
System.out.println("Hasil : " + hasil);
System.out.println("Kosong: " + kotak.kosong());
// Invariant terdokumentasi: ambil pada tumpukan kosong
try {
kotak.ambil();
} catch (NoSuchElementException e) {
System.out.println("Tertangkap: " + e.getMessage());
}
}
}Output yang Dihasilkan:
Ukuran: 3
Puncak: 30
Hasil : [30, 20, 10]
Kosong: true
Tertangkap: tumpukan kosongMari kita bedah pemanggilan pentingnya:
- Baris 10 (
kotak.dorongSemua(List.of(10, 20, 30))): argument bertipeList<Integer>masuk ke parameterIterable<? extends Number>karenaIntegeradalah turunanNumber. Tanpa wildcard, pemanggil harus membuatList<Number>baru terlebih dahulu. - Baris 18 (
List<Object> hasil):ambilSemuamenulis elemen bertipeNumber;List<Object>adalah consumer yang sah karenaObjectsupertype dariNumber. Invariant “tumpukan menjadi kosong” terdokumentasi di Javadoc dan terbukti pada baris keempat keluaran (Kosong: true). - Baris 24-28 (
kotak.ambil()dalamtry): pemanggilan pada tumpukan kosong melemparNoSuchElementExceptionsesuai kontrakambil— perilaku terdokumentasi, bukan kejutan. Baris terakhir keluaran mengonfirmasi pesannya.
Prinsip 4 Kembalikan Tipe Konkret #
Aturan PECS berhenti di parameter. Tipe kembalian sebaiknya selalu konkret — E, List<E>, Map<K, V> — bukan List<?> atau List<? extends E>. Pemanggil yang menerima List<?> kehilangan informasi: elemen hanya bisa dibaca sebagai Object, menulis dilarang, dan setiap operasi berguna menuntut cast. Method ambil() milik Tumpukan mengembalikan E sehingga Number puncak = kotak.intip() di baris 14 berjalan tanpa cast — informasi tipe mengalir penuh dari class ke pemanggil. Wildcard pada tipe kembalian hanya sah bila memang tidak ada informasi lebih kaya yang dapat diberikan; nyaris selalu ada.
Prinsip 5 Dokumentasikan Invariant #
Javadoc pada Tumpukan menuliskan tiga kelas kontrak: perilaku pada kondisi batas (tumpukan kosong melempar NoSuchElementException), kebijakan null (dorong menolak elemen null), dan efek samping (ambilSemua mengosongkan tumpukan). Tanpa dokumentasi ini, pemanggil menebak — dan tebakan pada API generik biasanya berujung pada ClassCastException di tempat yang jauh. Untuk API publik berumur panjang, kontrak tertulis adalah bagian dari desain tipe, setara pentingnya dengan signature-nya.
Generics menutup satu kebutuhan besar: struktur data dan alat yang bekerja untuk semua tipe dengan jaminan pemeriksaan kompilasi. Kebutuhan berikutnya mengalir dari pola yang sudah kita lihat — method salin menerima “sumber” dan “tujuan”, Comparator.comparing menerima method reference. Java menyebut nilai-nilai itu function object, dan dukungan bahasanya — lambda, Consumer<T>, Supplier<T>, hingga Function<T, R> — adalah wilayah Section Functional Programming yang menanti kita berikutnya.
Referensi #
- Java API: java.util.Deque
- Java API: java.util.Collection
- JLS SE25 §4.5: Type Arguments and Type Parameters
- dev.java: Generics
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiTumpukan.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class AplikasiTumpukan {
public static void main(String[] args) {
Tumpukan<Number> kotak = new Tumpukan<>();
// Producer: Iterable<Integer> masuk ke Tumpukan<Number>
kotak.dorongSemua(List.of(10, 20, 30));
System.out.println("Ukuran: " + kotak.ukuran());
// Tipe kembalian konkret: E = Number, bukan wildcard
Number puncak = kotak.intip();
System.out.println("Puncak: " + puncak);
// Consumer: isi mengalir ke List<Object>
List<Object> hasil = new ArrayList<>();
kotak.ambilSemua(hasil);
System.out.println("Hasil : " + hasil);
System.out.println("Kosong: " + kotak.kosong());
// Invariant terdokumentasi: ambil pada tumpukan kosong
try {
kotak.ambil();
} catch (NoSuchElementException e) {
System.out.println("Tertangkap: " + e.getMessage());
}
}
}
Tumpukan.java
Unduh berkasimport java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.NoSuchElementException;
public class Tumpukan<E> {
private final Deque<E> data = new ArrayDeque<>();
/**
* Menambahkan satu elemen ke puncak tumpukan.
* Elemen null tidak diterima.
*/
public void dorong(E elemen) {
if (elemen == null) {
throw new NullPointerException("elemen tidak boleh null");
}
data.push(elemen);
}
/**
* Menambahkan seluruh elemen sumber ke puncak tumpukan, berurutan
* dari elemen pertama hingga terakhir. Sumber hanya dibaca (producer),
* sehingga Iterable<? extends E> diterima.
*/
public void dorongSemua(Iterable<? extends E> sumber) {
for (E elemen : sumber) {
dorong(elemen);
}
}
/**
* Mengambil dan menghapus elemen puncak tumpukan.
* Melempar NoSuchElementException bila tumpukan kosong.
*/
public E ambil() {
if (data.isEmpty()) {
throw new NoSuchElementException("tumpukan kosong");
}
return data.pop();
}
/**
* Memindahkan seluruh isi tumpukan ke koleksi tujuan, mulai dari
* puncak hingga dasar. Tujuan hanya ditulis (consumer), sehingga
* Collection<? super E> diterima. Tumpukan menjadi kosong setelah
* pemanggilan berhasil.
*/
public void ambilSemua(Collection<? super E> tujuan) {
while (!data.isEmpty()) {
tujuan.add(ambil());
}
}
/**
* Mengintip elemen puncak tanpa menghapusnya.
* Melempar NoSuchElementException bila tumpukan kosong.
*/
public E intip() {
if (data.isEmpty()) {
throw new NoSuchElementException("tumpukan kosong");
}
return data.peek();
}
/** Banyaknya elemen saat ini. */
public int ukuran() {
return data.size();
}
/** Benar bila tumpukan tidak memiliki elemen. */
public boolean kosong() {
return data.isEmpty();
}
}