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.
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;
}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>.
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;
}
}Isi kotakKata : Kartu Anggota 041
Isi kotakAngka : 250Pada 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.
// 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 StringHubungan cetakan dan hasil pengisiannya digambarkan berikut ini.
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.
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) {
}Kaos Polos = 3
Rata-rata = 86.75Pada 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
Objectlalu memaksa tipe setiap kali nilainya dibaca. Pemeriksaan tipe berpindah ke saat program berjalan sehingga kekeliruan baru terlihat sebagaiClassCastException. 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 pesanrequired: reference. Gunakan pembungkusnya, yaituKotak<Integer>, dan biarkan autoboxing mengurus konversinya. - Memakai parameter tipe
Tpada kelas lain yang tidak mendeklarasikannya.Thanya dikenal di dalam kelas atau metode yang mendeklarasikannya, sehingga kegunaan di luar lingkup itu membuat kompilasi gagal dengan pesancannot find symbol. - Mengira parameter tipe mengonversi nilai. Menaruh
250keKotak<Integer>tidak mengubah apa pun, dan membaca dariKotak<String>tidak mengubah isi menjadi String. Parameter tipe hanya kontrak tentang tipe yang diizinkan. - Menyalin kelas wadah untuk setiap tipe, misalnya
KotakKatadanKotakAngkayang isinya mirip. Kedua kelas itu menggandakan kode yang harus dirawat bersama, dan setiap tipe baru menambah satu salinan lagi. Satu kelas genericKotak<T>menggantikan semuanya sekaligus.
Ringkasan#
- Kelas generic memakai parameter tipe seperti
Tagar 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#
- Lesson: Generics — Oracle — pengantar resmi generics beserta istilah parameter tipe dan argumen tipe.
- Generic Types — Oracle — sintaks deklarasi kelas generic dan konvensi penamaan parameter tipe.
- §8.1.2 Generic Classes and Type Parameters — Oracle — spesifikasi aturan deklarasi parameter tipe pada kelas.