Sepanjang Section ini kita menikmati kekuatan generics; kini kita petakan batas-batasnya. Semua larangan yang akan kita temui berakar pada satu sebab yang sama: type erasure. Karena informasi type argument dihapus dari bytecode, JVM tidak memiliki data untuk memeriksa T saat runtime — sehingga operasi yang membutuhkan pengetahuan tipe runtime pada type parameter tidak mungkin diizinkan oleh compiler javac. Alih-alih menghafal daftar larangan tanpa makna, kita akan verifikasi setiap pesan error-nya langsung di JDK 25, pahami alasan di baliknya, dan pelajari jalan keluar yang lazim dipakai pustaka standar. Pada akhirnya seluruh larangan tersebut terangkum dalam satu tabel ringkas siap dipakai saat berkoding.

Larangan Membuat Array Tipe Generik #

Ekspresi new T[ukuran] ditolak compiler karena array Java menyimpan tipe komponennya secara nyata di runtime — sementara T justru hilang setelah erasure. Verifikasi di JDK 25:

java
public class CekArray<T> {
    T[] buat(int ukuran) {
        return new T[ukuran];   // error
    }
}
text
CekArray.java:3: error: generic array creation
        return new T[ukuran];
               ^

Jalan keluar baku memakai type token: objek Class<T> dikirim sebagai constructor argument, lalu array dibuat lewat refleksi Array.newInstance. Solusi ini menuntut cast (T[]) yang diberi @SuppressWarnings("unchecked") karena compiler tidak dapat memverifikasinya:

java
import java.lang.reflect.Array;

class Wadah<T> {
    private final T[] data;

    Wadah(Class<T> tipe, int kapasitas) {
        @SuppressWarnings("unchecked")
        T[] hasil = (T[]) Array.newInstance(tipe, kapasitas);
        data = hasil;
    }
}

Mekanisme di baliknya dapat kita amati langsung tanpa cast yang meragukan:

import java.lang.reflect.Array;
import java.util.Arrays;

public class ArrayRefleksi {
    public static void main(String[] args) {
        // Array dibuat saat runtime: komponen diketahui dari objek Class
        String[] nama = (String[]) Array.newInstance(String.class, 3);
        nama[0] = "Budi";
        nama[1] = "Siti";
        nama[2] = "Dewi";
        System.out.println(Arrays.toString(nama));

        int[] angka = (int[]) Array.newInstance(int.class, 3);
        angka[0] = 7;
        angka[1] = 8;
        angka[2] = 9;
        System.out.println(Arrays.toString(angka));

        System.out.println(nama.getClass().getComponentType());
        System.out.println(angka.getClass().getComponentType());
    }
}

Output yang Dihasilkan:

text
[Budi, Siti, Dewi]
[7, 8, 9]
class java.lang.String
int

Dua baris terakhir keluaran memperlihatkan inti solusinya: getComponentType() melaporkan class java.lang.String untuk array pertama dan int untuk array kedua — tipe komponen benar-benar tercatat pada objek array, hal yang mustahil dilakukan type parameter. Mari kita bedah baris kuncinya:

  1. Baris 7 ((String[]) Array.newInstance(String.class, 3)): Array.newInstance menerima type token String.class dan kapasitas, lalu mengembalikan Object. Cast ke String[] aman karena type token menjamin tipe komponen, dan JVM tetap memeriksa cast array saat runtime.
  2. Baris 13 (Array.newInstance(int.class, 3)): type token berupa class primitive — int.class — menghasilkan array int[] sungguhan, bukan array wrapper, sehingga bebas boxing.
  3. Baris 19-20 (getComponentType()): kedua array melaporkan tipe komponennya masing-masing dari sisi runtime — bukti langsung bahwa tipe komponen array hidup di JVM, sementara type parameter tidak. ArrayList<E> memilih jalan berbeda: menyimpan Object[] secara internal lalu meng-cast setiap hasil get — itulah sebabnya pemeriksaan elemen ArrayList<String> tetap terjadi di titik baca.

Larangan instanceof dengan Tipe Parametrisasi #

Operator instanceof memeriksa tipe runtime, sedangkan List<String> tidak ada di runtime — pertentangan yang diselesaikan dengan melarang kombinasi keduanya:

java
Object o = List.of("a");
if (o instanceof List<String> daftar) {   // error
    System.out.println(daftar.size());
}
text
CekInstanceof.java:6: error: Object cannot be safely cast to List<String>
        if (o instanceof List<String> daftar) {
            ^

Pesan di JDK 25 berbunyi Object cannot be safely cast to List<String>; dokumentasi dan tulisan lawas sering mengutip pesan era JDK 8, illegal generic type for instanceof — keduanya menandai larangan yang sama. Batasan ini juga berlaku pada pattern matching untuk instanceof dan switch: pattern bertipe parametrisasi (misalnya case List<String> l) tidak diperbolehkan karena alasan identik. Yang tetap sah adalah wildcard polos:

import java.util.List;

public class CekInstanceofAman {
    public static void main(String[] args) {
        Object o = List.of("Budi", "Siti");
        if (o instanceof List<?> daftar) {   // wildcard polos: sah
            System.out.println("Ukuran: " + daftar.size());
            for (Object elemen : daftar) {
                System.out.println("Elemen: " + elemen);
            }
        }
    }
}

Output yang Dihasilkan:

text
Ukuran: 2
Elemen: Budi
Elemen: Siti

Pattern List<?> lolos karena pemeriksaannya hanya menyentuh class runtime List, bukan type argument-nya — konsekuensinya, elemen hanya terbaca sebagai Object, persis seperti perilaku List<?> pada Chapter Wildcards.

Larangan static Member dengan Type Parameter #

Kata kunci static berarti member milik class, bukan milik objek — sementara type parameter baru mendapat nilainya per instansiasi objek. Field static T akan menjadi ambigu: T milik Kotak<String> atau Kotak<Integer>?

java
public class CekStatic<T> {
    static T cadangan;   // error
}
text
CekStatic.java:2: error: non-static type variable T cannot be referenced from a static context
    static T cadangan;
           ^

Larangan yang sama berlaku untuk method static yang memakai type parameter class. Jika logika static memang butuh tipe generik, deklarasikan type parameter miliknya sendiri pada method tersebut — pola static <U> ... yang kita pelajari pada Chapter Generic Methods — sehingga U disimpulkan per pemanggilan, bukan per instansiasi.

Larangan Class Exception Generik #

Blok catch mencocokkan exception berdasarkan class runtime. Andaikan Galat<T> extends Exception diizinkan, erasure akan membuat catch (Galat<String>) dan catch (Galat<Integer>) menjadi dua label untuk class runtime yang sama — konflik yang dicegah sejak kompilasi:

java
public class Galat<T> extends Exception {   // error
}
text
CekGalat.java:1: error: a generic class may not extend java.lang.Throwable
public class CekGalat<T> extends Exception {
                                 ^

Idiom penggantinya adalah komposisi: class exception non-generik yang membawa payload bertipe apa pun di field, misalnya class Galat extends Exception { private final Object muatan; } — dan bila konteks pengguna sempit, subtype konkret seperti GalatString extends Galat menyimpan tipe spesifik tanpa menyentuh pewarisan generik.

Primitive Bukan Type Argument dan Larangan Lain #

Type argument wajib tipe referensi. List<int> ditolak dengan pesan yang menyebut syaratnya secara eksplisit:

text
CekPrimitif.java:5: error: unexpected type
        List<int> angka = null;
             ^
  required: reference
  found:    int

Solusinya adalah wrapper (List<Integer>) dengan biaya boxing, atau spesialisasi seperti IntStream untuk pipeline angka. Satu larangan terakhir kerap mengejutkan: dua overload yang dibedakan hanya oleh type argument ditolak karena hasil erasure-nya identik:

text
CekBentrok.java:5: error: name clash: tampil(List<Integer>) and tampil(List<String>) have the same erasure
    static void tampil(List<Integer> angka) { }
                ^

Berikut rangkuman seluruh larangan beserta alasannya:

LaranganPesan error kunciAlasan erasureJalan keluar lazim
new T[...]generic array creationtipe komponen array dicek runtimeArray.newInstance + Class<T>
x instanceof List<String>Object cannot be safely cast to List<String>type argument tak ada di runtimeinstanceof List<?>
static T fieldnon-static type variable T ...type parameter per instansiasitype parameter pada method static
class G<T> extends Throwablegeneric class may not extend java.lang.Throwablecatch mencocokkan class runtimekomposisi payload non-generik
List<int>unexpected type ... required: referencetype argument wajib referensiwrapper atau IntStream
overload List<A> vs List<B>name clash ... same erasuresignature terhapus identikbedakan nama method

Dengan peta rintangan lengkap di tangan, kita siap menuju puncak Section: merakit seluruh konsep — class, method, bound, wildcard, dan batasan erasure — menjadi satu API generik yang enak dipakai dan sulit disalahgunakan. Chapter Merancang Generic API menutup dengan studi kasus perancangan bertahap.

Referensi #

Berkas Sumber

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

ArrayRefleksi.java

Unduh berkas
java
import java.lang.reflect.Array;
import java.util.Arrays;

public class ArrayRefleksi {
    public static void main(String[] args) {
        // Array dibuat saat runtime: komponen diketahui dari objek Class
        String[] nama = (String[]) Array.newInstance(String.class, 3);
        nama[0] = "Budi";
        nama[1] = "Siti";
        nama[2] = "Dewi";
        System.out.println(Arrays.toString(nama));

        int[] angka = (int[]) Array.newInstance(int.class, 3);
        angka[0] = 7;
        angka[1] = 8;
        angka[2] = 9;
        System.out.println(Arrays.toString(angka));

        System.out.println(nama.getClass().getComponentType());
        System.out.println(angka.getClass().getComponentType());
    }
}

CekInstanceofAman.java

Unduh berkas
java
import java.util.List;

public class CekInstanceofAman {
    public static void main(String[] args) {
        Object o = List.of("Budi", "Siti");
        if (o instanceof List<?> daftar) {   // wildcard polos: sah
            System.out.println("Ukuran: " + daftar.size());
            for (Object elemen : daftar) {
                System.out.println("Elemen: " + elemen);
            }
        }
    }
}