Kelas Generic

Membuat kelas bertipe parameter seperti Box<T> dan memahami persoalan yang diselesaikan generics.

Daftar isi

Kelas generic, dalam istilah Java disebut generics, memungkinkan satu desain kelas dipakai untuk banyak tipe data dengan pemeriksaan tipe yang tetap berjalan saat kompilasi. Topik ini memandu pembuatan kelas bertipe parameter (type parameter), dimulai dari persoalan wadah bertipe Object, deklarasi Kotak<T>, sampai kelas dengan dua parameter tipe. Setelah menyelesaikannya, wadah data buatan sendiri dapat ditulis tanpa cast manual dan tanpa error tipe yang baru terlihat saat program berjalan.

Persoalan wadah bertipe Object#

Sebelum Java 5 memperkenalkan generics, wadah data ditulis dengan field bertipe Object, yaitu tipe yang menjadi leluhur semua kelas. Dengan Object, satu kelas wadah bisa menampung String, Integer, maupun tipe lain tanpa menulis ulang kelasnya. Kelonggaran itu membawa dua biaya sekaligus. Nilai yang diambil dari wadah wajib dipaksa (cast) ke tipe yang diharapkan, dan kekeliruan tipe tidak tertangkap sampai program berjalan.

Program berikut memasukkan Integer ke wadah Object, lalu membacanya dengan anggapan berupa String.

java UjiKotakObject.java
public class UjiKotakObject {

    public static void main(String[] args) {
        KotakObject kotak = new KotakObject();
        kotak.isi = 123;

        try {
            String kata = (String) kotak.isi;
            System.out.println(kata);
        } catch (ClassCastException e) {
            System.out.println("Tertangkap: " + e);
        }
    }
}

class KotakObject {
    Object isi;
}
text Keluaran
Tertangkap: java.lang.ClassCastException: 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')

Penugasan yang keliru itu lolos dari kompilator karena 123 memang boleh dimasukkan ke field bertipe Object. Kesalahan baru muncul pada baris cast, ketika Integer yang tersimpan dipaksa menjadi String. Penangkapan ClassCastException pada contoh sengaja dibuat agar program sempat mencetak pesannya, sedangkan tanpa penangkapan itu program berhenti dengan stack trace pada baris yang sama. Pada program kecil penyebabnya mudah dilacak, tetapi pada program besar baris yang salah menulis bisa berjarak ratusan baris dari tempat error muncul.

Mendeklarasikan kelas generic#

Kelas generic dideklarasikan dengan menambahkan parameter tipe di antara tanda < dan > tepat setelah nama kelas. Parameter tipe, misalnya T, berperan sebagai tempat kosong yang diisi tipe konkret ketika objek dibuat. Di dalam tubuh kelas, T boleh dipakai sebagai tipe field, tipe parameter metode, dan tipe nilai kembalian. Kode berikut mengubah wadah Object menjadi kelas generic Kotak<T>.

java UjiKotak.java
public class UjiKotak {

    public static void main(String[] args) {
        Kotak<String> kotakKata = new Kotak<>();
        kotakKata.letakkan("Kartu Anggota 041");
        String kartu = kotakKata.ambil();

        Kotak<Integer> kotakAngka = new Kotak<>();
        kotakAngka.letakkan(250);
        int jumlahPoin = kotakAngka.ambil();

        System.out.println("Isi kotakKata  : " + kartu);
        System.out.println("Isi kotakAngka : " + jumlahPoin);
    }
}

class Kotak<T> {
    private T isi;

    void letakkan(T isi) {
        this.isi = isi;
    }

    T ambil() {
        return isi;
    }
}
text Keluaran
Isi kotakKata  : Kartu Anggota 041
Isi kotakAngka : 250

Pada Kotak<String>, semua kemunculan T berlaku sebagai String, sehingga letakkan hanya menerima String dan ambil mengembalikan String tanpa cast. Pemanggilan new Kotak<>() memakai operator diamond, yaitu pasangan sudut kosong yang meminta kompilator menyimpulkan argumen tipenya (type argument) dari sisi kiri penugasan. Bila isi yang tidak sesuai dipaksakan, kompilasi langsung ditolak.

java Contoh: isi yang tidak sesuai langsung ditolak
// tidak-dijalankan
kotakKata.letakkan(123);
// error: method letakkan in class Kotak<T> cannot be applied to given types
//   reason: argument mismatch; int cannot be converted to String

Hubungan cetakan dan hasil pengisiannya digambarkan berikut ini.

Kelas Kotak dengan parameter tipe T dan dua hasil pengisiannyaclass Kotak<T>field isi bertipe TT diisi StringT diisi IntegerKotak<String>isi bertipe StringKotak<Integer>isi bertipe Integer
Kelas Kotak sebagai cetakan dan dua hasil pengisian parameter tipenya

Dua parameter tipe atau lebih#

Satu kelas generic boleh membawa lebih dari satu parameter tipe, dan setiap parameter mengikat satu tipe tersendiri. Bentuk ini lazim untuk struktur yang menggabungkan dua jenis data, misalnya pasangan kunci-nilai. Record Pasangan<K, V> berikut membawa kunci bertipe K dan nilai bertipe V sekaligus.

java UjiPasangan.java
public class UjiPasangan {

    public static void main(String[] args) {
        Pasangan<String, Integer> item = new Pasangan<>("Kaos Polos", 3);
        Pasangan<String, Double> hasil = new Pasangan<>("Rata-rata", 86.75);

        System.out.println(item.kunci() + " = " + item.nilai());
        System.out.println(hasil.kunci() + " = " + hasil.nilai());
    }
}

record Pasangan<K, V>(K kunci, V nilai) {
}
text Keluaran
Kaos Polos = 3
Rata-rata = 86.75

Pada pembuatan objek pertama, K menyimpulkan String dari argumen "Kaos Polos", sedangkan V menyimpulkan Integer dari 3. Pembuatan kedua menyimpulkan Double untuk V dari 86.75. Sejak itu, kunci() selalu mengembalikan String dan nilai() mengembalikan tipe yang sesuai dengan objek masing-masing. Bentuk dua parameter tipe seperti ini dipakai luas pada pustaka standar, misalnya Map<K, V>, dan berlaku pula pada interface serta record, bukan hanya class.

Urutan penulisan argumen tipe ikut menjadi bagian dari maknanya. Deklarasi Pasangan<Integer, String> berbeda dari Pasangan<String, Integer>, karena K selalu menampung argumen pertama dan V menampung argumen kedua. Karena itu konvensi penamaan K dan V dari kata kunci (key) dan nilai (value) membantu pembaca mengingat posisi masing-masing parameter. Ketika kebutuhannya bertambah, parameter tipe ketiga dan seterusnya cukup ditambahkan dengan dipisahkan koma.

Kesalahan umum#

  • Membuat wadah dengan field Object lalu memaksa tipe setiap kali nilainya dibaca. Pemeriksaan tipe berpindah ke saat program berjalan sehingga kekeliruan baru terlihat sebagai ClassCastException. Jadikan wadahnya kelas generic agar pilihan tipenya diperiksa saat kompilasi.
  • Menulis argumen tipe dengan tipe primitif seperti Kotak<int>. Argumen tipe hanya boleh tipe referensi, sehingga kompilator menolaknya dengan pesan required: reference. Gunakan pembungkusnya, yaitu Kotak<Integer>, dan biarkan autoboxing mengurus konversinya.
  • Memakai parameter tipe T pada kelas lain yang tidak mendeklarasikannya. T hanya dikenal di dalam kelas atau metode yang mendeklarasikannya, sehingga kegunaan di luar lingkup itu membuat kompilasi gagal dengan pesan cannot find symbol.
  • Mengira parameter tipe mengonversi nilai. Menaruh 250 ke Kotak<Integer> tidak mengubah apa pun, dan membaca dari Kotak<String> tidak mengubah isi menjadi String. Parameter tipe hanya kontrak tentang tipe yang diizinkan.
  • Menyalin kelas wadah untuk setiap tipe, misalnya KotakKata dan KotakAngka yang isinya mirip. Kedua kelas itu menggandakan kode yang harus dirawat bersama, dan setiap tipe baru menambah satu salinan lagi. Satu kelas generic Kotak<T> menggantikan semuanya sekaligus.

Ringkasan#

  • Kelas generic memakai parameter tipe seperti T agar field, parameter metode, dan nilai kembalian mengikuti tipe yang dipilih saat objek dibuat.
  • Pemeriksaan tipe berpindah ke waktu kompilasi, sehingga isi yang tidak sesuai ditolak sebelum program sempat dijalankan.
  • Operator diamond <> meminta kompilator menyimpulkan argumen tipe dari sisi kiri penugasan.
  • Satu kelas boleh membawa beberapa parameter tipe, dan setiap parameter mengikat satu tipe tersendiri.

Referensi#