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:

text
java.util.ArrayList
java.util.ArrayList
true
Kotak
Kotak

Dua 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():

text
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/String

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

text
Note: PolusiTertangkap.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Menjalankan ulang dengan javac -Xlint:unchecked membuka detailnya:

text
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

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

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

  1. Baris 7 (List mentah = nama): variabel mentah bertipe raw List menunjuk objek ArrayList yang sama dengan nama. Karena erasure, keduanya memang satu objek runtime — type argument hanya hidup di dunia compiler.
  2. Baris 8 (mentah.add(Integer.valueOf(42))): lewat raw type, compiler tidak memeriksa argument terhadap String; nilai Integer masuk ke daftar yang dideklarasikan List<String>. Inilah titik polusi — dan peringatan unchecked call to add(E) pada output menandainya.
  3. Baris 10 (String s = nama.get(0)): signature get setelah erasure mengembalikan Object, lalu compiler menyisipkan cast ke String (ingat checkcast di bytecode). Cast gagal karena isinya Integer, dan ClassCastException terangkat — 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:

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

Berkas Sumber

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

BuktiErasure.java

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

Kotak.java

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

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

    public T ambil() {
        return isi;
    }
}

PolusiTertangkap.java

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