Bayangkan sebuah wadah yang bisa menampung apa saja: nama orang, angka, bahkan objek buatan sendiri. Java versi awal menyediakan pola seperti itu lewat tipe Object, tetapi pola tersebut menuntut cast manual setiap kali isi diambil kembali dan membuka peluang ClassCastException saat program berjalan. Generics, yang hadir sejak Java 5, mengubah keadaan ini dengan cara memparametrisasi tipe: kita menuliskan tipe yang diinginkan saat membuat objek, lalu compiler javac menjaga kesesuaiannya di seluruh kode. Pada Chapter ini kita mulai dari fondasinya: mendeklarasikan generic class, memahami pemeriksaan tipe yang dilakukan compiler, dan membandingkan pendekatan generik dengan wadah berbasis Object secara langsung.
Wadah Berbasis Object dan Masalahnya #
Sebelum generics tersedia, class wadah ditulis dengan field bertipe Object. Karena semua class di Java merupakan turunan Object, wadah semacam itu menerima referensi apa pun tanpa protes. Kebebasan tersebut berubah menjadi beban saat mengambil isi: nilai yang keluar bertipe Object, sehingga kita wajib melakukan cast eksplisit ke tipe yang diyakini benar. Bila keyakinan tersebut salah, kegagalan baru muncul jauh setelah kompilasi — tepat pada saat program berjalan.
public class KotakRaw {
public static void main(String[] args) {
KotakObject kotak = new KotakObject();
kotak.simpan("Budi");
String nama = (String) kotak.ambil(); // cast eksplisit wajib
System.out.println("Nama : " + nama);
kotak.simpan(Integer.valueOf(42)); // kompiler tidak protes
try {
String salah = (String) kotak.ambil(); // gagal saat runtime
System.out.println("Isi : " + salah);
} catch (ClassCastException e) {
System.out.println("Gagal: " + e.getMessage());
}
}
}
class KotakObject {
private Object isi;
public void simpan(Object nilai) {
isi = nilai;
}
public Object ambil() {
return isi;
}
}Output yang Dihasilkan:
Nama : Budi
Gagal: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')Mari kita bedah peristiwa penting pada program di atas:
- Baris 4-5 (
kotak.simpan("Budi")lalu cast): methodsimpanmenerimaObject, sehingga string masuk tanpa diperiksa. Cast(String)pada baris kelima berhasil karena isinya memang string. - Baris 9 (
kotak.simpan(Integer.valueOf(42))): compilerjavacmenerima baris ini tanpa keluhan — parameter bertipeObjectmenghalalkan referensiInteger. Kesalahan logika ini lolos dari pemeriksaan kompilasi. - Baris 11 (
(String) kotak.ambil()): JVM memeriksa tipe objek saat cast berjalan. Objek yang tersimpan bertipeInteger, cast keStringgagal, dan JVM melemparClassCastExceptionyang kemudian kita tangkap untuk ditampilkan.
Pesannya jelas: kesalahan tipe pada wadah berbasis Object tidak terdeteksi saat kompilasi, melainkan muncul sebagai exception saat runtime — biasanya jauh dari baris yang menjadi sumber masalah.
Class Generik dengan Type Parameter #
Generic class memindahkan tanggung jawab pemeriksaan tipe dari runtime ke kompilasi. Deklarasi class Kotak<T> memperkenalkan type parameter bernama T — semacam variabel untuk tipe yang nilainya baru ditentukan saat objek dibuat. Method simpan dan ambil kemudian memakai T sebagai tipe parameter dan tipe kembalian, sehingga satu definisi class otomatis bekerja untuk Kotak<String>, Kotak<Integer>, atau tipe lain.
public class Kotak<T> {
private T isi;
public void simpan(T nilai) {
isi = nilai;
}
public T ambil() {
return isi;
}
}Berkas di atas adalah berkas definisi tanpa main, siap dipakai program lain di bundle yang sama. Berikut pemakaiannya:
public class KotakGenerik {
public static void main(String[] args) {
Kotak<String> kotakNama = new Kotak<>();
kotakNama.simpan("Budi");
String nama = kotakNama.ambil(); // tanpa cast
Kotak<Integer> kotakAngka = new Kotak<>();
kotakAngka.simpan(42);
int angka = kotakAngka.ambil(); // tanpa cast + auto-unboxing
System.out.println("Nama : " + nama);
System.out.println("Angka: " + angka);
System.out.println("Class: " + kotakNama.getClass().getName());
System.out.println("Class: " + kotakAngka.getClass().getName());
}
}Output yang Dihasilkan:
Nama : Budi
Angka: 42
Class: Kotak
Class: KotakPerhatikan tiga perubahan penting dibandingkan versi Object:
- Baris 4 dan 9 (
new Kotak<>()): kita menyebut type argument saat membuat objek. Compiler mengganti setiap kemunculanTdalam definisi class dengan tipe tersebut —StringuntukkotakNama,IntegeruntukkotakAngka. - Baris 6 dan 11 (
kotakNama.ambil()dankotakAngka.ambil()): hasilambil()sudah bertipe tepat (StringdanInteger), sehingga cast eksplisit tidak diperlukan lagi. Baris 11 bahkan menikmati auto-unboxing dariIntegerkeintsecara langsung. - Baris 14-15 (
getClass().getName()): kedua objek mencetak nama class yang sama,Kotak. Informasi type argument tidak tersimpan di objek saat runtime — fenomena type erasure yang kita bedah tuntas pada Chapter khusus nanti.
Kesalahan Tipe Tertangani Saat Kompilasi #
Bagian terpenting dari generics bukan syntax, melainkan jaminan keamanannya. Program berikut sengaja menyimpan string ke dalam Kotak<Integer>:
public class KotakSalah {
public static void main(String[] args) {
Kotak<Integer> kotakAngka = new Kotak<>();
kotakAngka.simpan("halo");
}
}Compiler javac menolak program tersebut sebelum JVM sempat berjalan dengan pesan yang kita verifikasi langsung:
KotakSalah.java:4: error: method simpan in class Kotak<T> cannot be applied to given types;
required: Integer
found: String
reason: argument mismatch; String cannot be converted to IntegerCompiler membaca type argument Integer, mensubstitusikan T pada signature simpan(T) menjadi simpan(Integer), lalu mendapati argument "halo" tidak cocok. Bandingkan dengan versi Object: kesalahan serupa di sana lolos kompilasi dan baru terasa sebagai ClassCastException saat runtime — sering kali di lingkungan production, jauh dari baris penyebabnya.
Konvensi Nama Type Parameter #
Nama type parameter bebas sepanjang valid sebagai identifier, namun ekosistem Java menganut konvensi satu huruf yang bermakna:
| Huruf | Singkatan | Contoh pemakaian |
|---|---|---|
T | Type | Kotak<T> — tipe umum |
E | Element | List<E> — elemen collection |
K, V | Key, Value | Map<K, V> — pasangan kunci-nilai |
R | Result | Callable<V> mengembalikan hasil |
U, S | penanda tambahan | Pair<U, S> — type parameter kedua |
Konvensi ini murni keterbacaan; Kotak<T> dan Kotak<X> sah secara bahasa dan berperilaku identik.
Dengan fondasi generic class terpasang, pertanyaan berikutnya muncul secara alami: apakah seluruh class harus digeneralkan hanya demi satu method yang serba bisa? Tidak perlu. Java memungkinkan method berdiri sendiri dengan type parameter miliknya sendiri — pendekatan yang lebih ringkas untuk banyak kasus, dan menjadi fokus Chapter Generic Methods.
Referensi #
- Oracle: Generics (Java Tutorials)
- dev.java: Generics
- JLS SE25 §4.5: Type Arguments and Type Parameters
- Java API: java.util.Collection
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
Kotak.java
Unduh berkaspublic class Kotak<T> {
private T isi;
public void simpan(T nilai) {
isi = nilai;
}
public T ambil() {
return isi;
}
}
KotakGenerik.java
Unduh berkaspublic class KotakGenerik {
public static void main(String[] args) {
Kotak<String> kotakNama = new Kotak<>();
kotakNama.simpan("Budi");
String nama = kotakNama.ambil(); // tanpa cast
Kotak<Integer> kotakAngka = new Kotak<>();
kotakAngka.simpan(42);
int angka = kotakAngka.ambil(); // tanpa cast + auto-unboxing
System.out.println("Nama : " + nama);
System.out.println("Angka: " + angka);
System.out.println("Class: " + kotakNama.getClass().getName());
System.out.println("Class: " + kotakAngka.getClass().getName());
}
}
KotakRaw.java
Unduh berkaspublic class KotakRaw {
public static void main(String[] args) {
KotakObject kotak = new KotakObject();
kotak.simpan("Budi");
String nama = (String) kotak.ambil(); // cast eksplisit wajib
System.out.println("Nama : " + nama);
kotak.simpan(Integer.valueOf(42)); // kompiler tidak protes
try {
String salah = (String) kotak.ambil(); // gagal saat runtime
System.out.println("Isi : " + salah);
} catch (ClassCastException e) {
System.out.println("Gagal: " + e.getMessage());
}
}
}
class KotakObject {
private Object isi;
public void simpan(Object nilai) {
isi = nilai;
}
public Object ambil() {
return isi;
}
}
KotakSalah.java
Unduh berkaspublic class KotakSalah {
public static void main(String[] args) {
Kotak<Integer> kotakAngka = new Kotak<>();
kotakAngka.simpan("halo");
}
}