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.

java KotakRaw.java
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:

text
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:

  1. Baris 4-5 (kotak.simpan("Budi") lalu cast): method simpan menerima Object, sehingga string masuk tanpa diperiksa. Cast (String) pada baris kelima berhasil karena isinya memang string.
  2. Baris 9 (kotak.simpan(Integer.valueOf(42))): compiler javac menerima baris ini tanpa keluhan — parameter bertipe Object menghalalkan referensi Integer. Kesalahan logika ini lolos dari pemeriksaan kompilasi.
  3. Baris 11 ((String) kotak.ambil()): JVM memeriksa tipe objek saat cast berjalan. Objek yang tersimpan bertipe Integer, cast ke String gagal, dan JVM melempar ClassCastException yang 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.

java Kotak.java
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:

text
Nama : Budi
Angka: 42
Class: Kotak
Class: Kotak

Perhatikan tiga perubahan penting dibandingkan versi Object:

  1. Baris 4 dan 9 (new Kotak<>()): kita menyebut type argument saat membuat objek. Compiler mengganti setiap kemunculan T dalam definisi class dengan tipe tersebut — String untuk kotakNama, Integer untuk kotakAngka.
  2. Baris 6 dan 11 (kotakNama.ambil() dan kotakAngka.ambil()): hasil ambil() sudah bertipe tepat (String dan Integer), sehingga cast eksplisit tidak diperlukan lagi. Baris 11 bahkan menikmati auto-unboxing dari Integer ke int secara langsung.
  3. 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>:

java KotakSalah.java
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:

text
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 Integer

Compiler 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:

HurufSingkatanContoh pemakaian
TTypeKotak<T> — tipe umum
EElementList<E> — elemen collection
K, VKey, ValueMap<K, V> — pasangan kunci-nilai
RResultCallable<V> mengembalikan hasil
U, Spenanda tambahanPair<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 #

Berkas Sumber

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

Kotak.java

Unduh berkas
java
public class Kotak<T> {
    private T isi;

    public void simpan(T nilai) {
        isi = nilai;
    }

    public T ambil() {
        return isi;
    }
}

KotakGenerik.java

Unduh berkas
java
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());
    }
}

KotakRaw.java

Unduh berkas
java
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;
    }
}

KotakSalah.java

Unduh berkas
java
public class KotakSalah {
    public static void main(String[] args) {
        Kotak<Integer> kotakAngka = new Kotak<>();
        kotakAngka.simpan("halo");
    }
}