Chapter sebelumnya menempatkan type parameter pada class: satu Kotak<T>, satu wadah, satu tipe. Sering kali kebutuhannya lebih sempit dari itu — hanya satu method yang perlu bekerja untuk berbagai tipe, sementara class di sekelilingnya tidak. Java menyediakan generic method: method yang mendeklarasikan type parameter miliknya sendiri, ditulis sebelum tipe kembalian. Compiler javac kemudian menyimpulkan tipe tersebut dari argument aktual pada setiap pemanggilan, tanpa kita menuliskannya secara manual. Pada Chapter ini kita pelajari bentuk deklarasinya, cara kerja inferensi tipe, dan momen ketika type parameter perlu dinyatakan eksplisit sebagai type witness.

Deklarasi Method Generik #

Bentuk umumnya adalah modifier <T> TipeKembalian namaMethod(parameter). Bagian <T> mendeklarasikan type parameter yang hanya hidup di dalam method tersebut — parameter, tipe kembalian, dan isi body boleh memakainya. Penanda ini wajib ditulis sebelum tipe kembalian karena compiler membaca deklarasi dari kiri ke kanan; tanpa <T>, huruf T pada T pertama(...) tidak bermakna apa pun.

Generic method dapat berupa method static, method instance, bahkan constructor. Berikut tiga method generik dengan pola pemakaian T yang berbeda — sebagai tipe kembalian, sebagai tipe varargs, dan sebagai tipe parameter saja:

java AlatDaftar.java
import java.util.ArrayList;
import java.util.List;

public class AlatDaftar {
    // T disimpulkan dari tipe argument saat pemanggilan
    static <T> T pertama(List<T> daftar) {
        return daftar.get(0);
    }

    // T dipakai pada parameter varargs sekaligus tipe kembalian
    @SafeVarargs
    static <T> List<T> dari(T... nilai) {
        List<T> hasil = new ArrayList<>();
        for (T t : nilai) {
            hasil.add(t);
        }
        return hasil;
    }

    // T hanya muncul di parameter — method tanpa nilai balik
    static <T> void tukar(List<T> daftar, int i, int j) {
        T sementara = daftar.get(i);
        daftar.set(i, daftar.get(j));
        daftar.set(j, sementara);
    }

    public static void main(String[] args) {
        List<String> nama = List.of("Budi", "Siti", "Dewi");
        String depan = pertama(nama);           // T disimpulkan String
        System.out.println("Pertama : " + depan);

        List<Integer> angka = List.of(10, 20, 30);
        Integer awal = pertama(angka);          // T disimpulkan Integer
        System.out.println("Awal    : " + awal);

        Integer eksplisit = AlatDaftar.<Integer>pertama(angka);
        System.out.println("Witness : " + eksplisit);

        List<String> bulan = dari("Jan", "Feb", "Mar");
        System.out.println("Bulan   : " + bulan);

        List<String> kota = new ArrayList<>(List.of("Jakarta", "Bandung"));
        tukar(kota, 0, 1);
        System.out.println("Tukar   : " + kota);
    }
}

Output yang Dihasilkan:

text
Pertama : Budi
Awal    : 10
Witness : 10
Bulan   : [Jan, Feb, Mar]
Tukar   : [Bandung, Jakarta]

Mari kita bedah method dan pemanggilannya satu per satu:

  1. Baris 6-8 (static <T> T pertama(List<T> daftar)): T muncul di dua tempat — tipe kembalian dan elemen List. Satu deklarasi <T> mengikat keduanya, sehingga method ini menjanjikan: “nilai yang dikembalikan bertipe sama dengan elemen daftar”.
  2. Baris 11-18 (static <T> List<T> dari(T... nilai)): varargs T... nilai) memakai type parameter, dan anotasi @SafeVarargs` menenangkan compiler terkait peringatan khusus varargs generik (kita bahas tuntas pada Chapter Type Erasure).
  3. Baris 21-26 (static <T> void tukar(...)): T dipakai untuk variabel sementara dan operasi set, tanpa tipe kembalian — bukti type parameter tidak wajib muncul pada nilai balik.
  4. Baris 29-34 (pemanggilan pertama): pemanggilan pertama mengirim List<String> sehingga T = String; pemanggilan kedua mengirim List<Integer> sehingga T = Integer. Satu definisi method melayani dua tipe tanpa duplikasi kode dan tanpa cast.

Satu catatan lingkup penting: type parameter method berbeda dari type parameter class. Method tukar di atas dapat dipindahkan ke dalam class generik Daftar<T> tanpa masalah, tetapi bila keduanya memakai huruf yang sama, type parameter method akan menutupi (shadow) type parameter class di dalam body method — perilaku sah secara bahasa namun mudah mengecohkan pembaca. Konvensi yang aman: gunakan huruf berbeda, misalnya <U>, untuk generic method yang hidup di dalam generic class.

Inferensi Tipe dari Argument #

Mekanisme di balik layar bekerja seperti berikut: compiler javac memeriksa setiap argument pada lokasi pemanggilan, lalu menyelaraskan tipe argument dengan posisi type parameter pada signature. Proses ini juga memperhitungkan konteks assignment — tipe variabel yang menerima nilai kembalian ikut menjadi petunjuk. Hasil inferensi bersifat lokal per pemanggilan: T pada pemanggilan pertama dan T pada pemanggilan kedua tidak saling terkait.

Inferensi bukan sekadar kenyamanan; inferensi juga penjaga keamanan tipe. Program berikut mencoba menampung hasil pertama(angka) bertipe Integer ke variabel String:

import java.util.List;

public class InferensiSalah {
    static <T> T pertama(List<T> daftar) {
        return daftar.get(0);
    }

    public static void main(String[] args) {
        List<Integer> angka = List.of(10, 20);
        String s = pertama(angka);   // T terjepit dua syarat yang berlawanan
        System.out.println(s);
    }
}

Compiler javac menolaknya dengan pesan verbatim berikut:

text
InferensiSalah.java:10: error: incompatible types: inference variable T has incompatible bounds
    equality constraints: Integer
    upper bounds: String,Object

Pesan tersebut memperlihatkan cara compiler bekerja: argument List<Integer> memaksa T = Integer (equality constraint), sedangkan assignment ke String s memaksa hasil T berupa String (upper bound). Kedua syarat tidak mungkin dipenuhi bersamaan, sehingga kompilasi dihentikan sebelum JVM berjalan.

Type Witness Eksplisit #

Kadang kita perlu mendikte type parameter alih-alih menyerahkan pada inferensi — misalnya saat argument tidak memberi petunjuk cukup atau saat hasil inferensi kurang spesifik dari kebutuhan. Java menyediakan type witness: penulisan type argument eksplisit di antara nama class/method dan argument pemanggilan, seperti AlatDaftar.<Integer>pertama(angka) pada berkas AlatDaftar.java baris 36. Sintaks yang sama bekerja untuk method pada class lain, misalnya Collections.<String>emptyList(). Tanpa petunjuk apa pun, pemanggilan Collections.emptyList() yang berdiri sendiri disimpulkan sebagai List<Object>; witness eksplisit atau konteks assignment-lah yang mengarahkannya menjadi List<String>.

Type witness yang salah arah langsung ditolak compiler. Uji di JDK 25 memperlihatkan pemanggilan PertamaBantu.<String>pertama(angka) dengan angka bertipe List<Integer> gagal dikompilasi dengan error: incompatible types: List<Integer> cannot be converted to List<String> — argument dan witness harus konsisten, sama seperti pada generic class.

Generic method dan generic class bertemu pada satu titik: interface. Kontrak seperti Comparable<T> atau Penyimpan<T> memparametrisasi seluruh perilaku sebuah tipe, dan class yang mengimplementasikannya boleh mengunci type parameter menjadi tipe konkret atau meneruskannya lebih lanjut. Kombinasi inilah yang kita bahas pada Chapter Generic Interfaces.

Referensi #

Berkas Sumber

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

AlatDaftar.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class AlatDaftar {
    // T disimpulkan dari tipe argument saat pemanggilan
    static <T> T pertama(List<T> daftar) {
        return daftar.get(0);
    }

    // T dipakai pada parameter varargs sekaligus tipe kembalian
    @SafeVarargs
    static <T> List<T> dari(T... nilai) {
        List<T> hasil = new ArrayList<>();
        for (T t : nilai) {
            hasil.add(t);
        }
        return hasil;
    }

    // T hanya muncul di parameter — method tanpa nilai balik
    static <T> void tukar(List<T> daftar, int i, int j) {
        T sementara = daftar.get(i);
        daftar.set(i, daftar.get(j));
        daftar.set(j, sementara);
    }

    public static void main(String[] args) {
        List<String> nama = List.of("Budi", "Siti", "Dewi");
        String depan = pertama(nama);           // T disimpulkan String
        System.out.println("Pertama : " + depan);

        List<Integer> angka = List.of(10, 20, 30);
        Integer awal = pertama(angka);          // T disimpulkan Integer
        System.out.println("Awal    : " + awal);

        Integer eksplisit = AlatDaftar.<Integer>pertama(angka);
        System.out.println("Witness : " + eksplisit);

        List<String> bulan = dari("Jan", "Feb", "Mar");
        System.out.println("Bulan   : " + bulan);

        List<String> kota = new ArrayList<>(List.of("Jakarta", "Bandung"));
        tukar(kota, 0, 1);
        System.out.println("Tukar   : " + kota);
    }
}

InferensiSalah.java

Unduh berkas
java
import java.util.List;

public class InferensiSalah {
    static <T> T pertama(List<T> daftar) {
        return daftar.get(0);
    }

    public static void main(String[] args) {
        List<Integer> angka = List.of(10, 20);
        String s = pertama(angka);   // T terjepit dua syarat yang berlawanan
        System.out.println(s);
    }
}