Kita sudah memakai generics sepanjang Section ini — kini saatnya membuka kap mesinnya. Java menerapkan generics lewat type erasure: compiler javac memakai informasi type argument untuk memeriksa kode, lalu menghapusnya saat membuat bytecode. Hasilnya, List<String> dan List<Integer> menjadi class runtime yang sama persis; tidak ada List<String>.class di JVM. Keputusan ini diambil pada Java 5 agar kode generik dan kode lawas non-generik dapat berjalan pada JVM yang sama tanpa kompilasi ulang — kompatibilitas migrasi yang menjadi warisan hingga kini. Pada Chapter ini kita buktikan erasure lewat getClass(), lihat bagaimana compiler mengganti T dengan batasnya dan menyisipkan cast, lalu jumpai dua akibat langsungnya: unchecked warning pada raw type dan heap pollution yang diam-diam.
Bukti Erasure di Runtime #
Kalau type argument benar-benar tersimpan di objek, dua daftar dengan type argument berbeda akan melaporkan class yang berbeda. Program berikut menguji dugaan itu pada ArrayList dari pustaka standar sekaligus Kotak buatan kita:
import java.util.ArrayList;
import java.util.List;
public class BuktiErasure {
public static void main(String[] args) {
List<String> nama = new ArrayList<>();
List<Integer> angka = new ArrayList<>();
System.out.println(nama.getClass().getName());
System.out.println(angka.getClass().getName());
System.out.println(nama.getClass() == angka.getClass());
Kotak<String> kotakNama = new Kotak<>();
Kotak<Integer> kotakAngka = new Kotak<>();
System.out.println(kotakNama.getClass().getName());
System.out.println(kotakAngka.getClass().getName());
}
}Output yang Dihasilkan:
java.util.ArrayList
java.util.ArrayList
true
Kotak
KotakDua pengamatan penting dari keluaran tersebut. Pertama, nama.getClass().getName() dan angka.getClass().getName() sama-sama mencetak java.util.ArrayList — nama class runtime tidak memuat <String> maupun <Integer>. Kedua, perbandingan nama.getClass() == angka.getClass() menghasilkan true: satu objek Class yang identik mewakili kedua “tipe” tersebut. Baris kelima dan keenam mengulang kesimpulan yang sama pada class buatan sendiri: Kotak<String> dan Kotak<Integer> sama-sama hanya Kotak.
Bagaimana Compiler Menghapus Tipe #
Erasure bekerja dengan dua aturan sederhana. Type parameter tanpa bound dihapus menjadi Object; type parameter dengan bound dihapus menjadi bound pertamanya — T extends Number menjadi Number. Seluruh signature method ikut berubah: simpan(T) menjadi simpan(Object) dan ambil() mengembalikan Object pada bytecode. Lalu compiler menyisipkan cast pada setiap lokasi yang memakai hasil method generik sebagai tipe spesifik. Bukti bytecode berikut kita ambil dengan javap -c dari class hasil kompilasi program yang memanggil KotakB<String>.ambil():
9: ldc #10 // String Budi
11: invokevirtual #12 // Method KotakB.simpan:(Ljava/lang/Object;)V
15: invokevirtual #16 // Method KotakB.ambil:()Ljava/lang/Object;
18: checkcast #20 // class java/lang/StringTiga baris tersebut menceritakan seluruh cerita: simpan dipanggil dengan parameter Ljava/lang/Object;, ambil mengembalikan Ljava/lang/Object;, dan instruksi checkcast pada offset 18 adalah cast ke String yang ditulis compiler — bukan oleh kita. Cast inilah yang melempar ClassCastException ketika isi wadah ternyata tidak sesuai deklarasi.
Raw Type dan Peringatan Unchecked #
Masa transisi Java 5 membiarkan kode lawas memakai List tanpa type argument — disebut raw type. Raw type tetap sah dipakai, tetapi compiler tidak lagi dapat menjamin keamanan tipe pada titik temu antara kode generik dan kode raw. Sebagai peringatan, javac mencetak catatan yang kita verifikasi langsung:
Note: PolusiTertangkap.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.Menjalankan ulang dengan javac -Xlint:unchecked membuka detailnya:
PolusiTertangkap.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
mentah.add(Integer.valueOf(42)); // heap pollution terjadi di sini
^
where E is a type-variable:
E extends Object declared in interface ListPeringatan unchecked bermakna: “operasi ini melewati pemeriksaan tipe; tanggung jawab berada di tangan Anda”.
Heap Pollution dalam Aksi #
Heap pollution terjadi ketika variabel bertipe parametrisasi menunjuk objek yang berisi elemen di luar type argument-nya. Program berikut memicunya lewat variabel raw, lalu menangkap akibatnya — blok output di bawah digabung dari stdout dan stderr sehingga peringatan kompilasi ikut terlihat:
import java.util.ArrayList;
import java.util.List;
public class PolusiTertangkap {
public static void main(String[] args) {
List<String> nama = new ArrayList<>();
List mentah = nama; // konversi ke raw type: unchecked
mentah.add(Integer.valueOf(42)); // heap pollution terjadi di sini
try {
String s = nama.get(0); // cast implisit hasil erasure
System.out.println("Isi: " + s);
} catch (ClassCastException e) {
System.out.println("Tertangkap: " + e.getMessage());
}
}
}Output yang Dihasilkan:
PolusiTertangkap.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
mentah.add(Integer.valueOf(42)); // heap pollution terjadi di sini
^
where E is a type-variable:
E extends Object declared in interface List
1 warning
Tertangkap: 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 telusuri alurnya baris demi baris:
- Baris 7 (
List mentah = nama): variabelmentahbertipe rawListmenunjuk objekArrayListyang sama dengannama. Karena erasure, keduanya memang satu objek runtime — type argument hanya hidup di dunia compiler. - Baris 8 (
mentah.add(Integer.valueOf(42))): lewat raw type, compiler tidak memeriksa argument terhadapString; nilaiIntegermasuk ke daftar yang dideklarasikanList<String>. Inilah titik polusi — dan peringatanunchecked call to add(E)pada output menandainya. - Baris 10 (
String s = nama.get(0)): signaturegetsetelah erasure mengembalikanObject, lalu compiler menyisipkan cast keString(ingatcheckcastdi bytecode). Cast gagal karena isinyaInteger, danClassCastExceptionterangkat — persis di titik baca, jauh dari titik tulis yang menjadi sumber masalah.
Bridge Method Warisan Erasure #
Erasure menciptakan satu persoalan halus pada pewarisan. Class anak yang meng-override method generik dengan tipe konkret, misalnya setKiri(String), seolah tidak lagi cocok dengan signature induk yang telah terhapus menjadi setKiri(Object). Compiler menjahitnya dengan bridge method — method sintetis ber-signature terhapus yang mendelegasikan ke override kita. Bukti dari javap PasanganString pada JDK 25:
Compiled from "Pasangan.java"
class PasanganString extends Pasangan<java.lang.String> {
PasanganString();
public void setKiri(java.lang.String);
public void setKiri(java.lang.Object);
}Method kedua, setKiri(java.lang.Object), tidak pernah kita tulis — compiler mensintesisnya berisi panggilan ke setKiri(java.lang.String). Inilah yang membuat polymorphism generik tetap berfungsi: panggilan lewat variabel Pasangan<String> (yang setelah erasure memanggil versi Object) mendarat di bridge, lalu diteruskan ke override yang bertipe tepat.
Erasure menjelaskan banyak hal yang sebelumnya kita terima mentah-mentah: mengapa getClass() identik, mengapa cast muncul di bytecode, mengapa raw type berbahaya. Pertanyaan berikutnya justru tentang kenyamanan: bagaimana compiler menyimpulkan type argument yang tidak kita tulis — dari diamond <> sampai var — tanpa kehilangan ketelitian? Itu topik Chapter Generic Type Inference.
Referensi #
- Oracle: Type Erasure (Java Tutorials)
- JLS SE25 §4.6: Type Erasure
- JLS SE25 §4.12.2: Variables and Reference Types (heap pollution)
- dev.java: Generics
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BuktiErasure.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class BuktiErasure {
public static void main(String[] args) {
List<String> nama = new ArrayList<>();
List<Integer> angka = new ArrayList<>();
System.out.println(nama.getClass().getName());
System.out.println(angka.getClass().getName());
System.out.println(nama.getClass() == angka.getClass());
Kotak<String> kotakNama = new Kotak<>();
Kotak<Integer> kotakAngka = new Kotak<>();
System.out.println(kotakNama.getClass().getName());
System.out.println(kotakAngka.getClass().getName());
}
}
Kotak.java
Unduh berkaspublic class Kotak<T> {
private T isi;
public void simpan(T nilai) {
isi = nilai;
}
public T ambil() {
return isi;
}
}
PolusiTertangkap.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class PolusiTertangkap {
public static void main(String[] args) {
List<String> nama = new ArrayList<>();
List mentah = nama; // konversi ke raw type: unchecked
mentah.add(Integer.valueOf(42)); // heap pollution terjadi di sini
try {
String s = nama.get(0); // cast implisit hasil erasure
System.out.println("Isi: " + s);
} catch (ClassCastException e) {
System.out.println("Tertangkap: " + e.getMessage());
}
}
}