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:
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:
Pertama : Budi
Awal : 10
Witness : 10
Bulan : [Jan, Feb, Mar]
Tukar : [Bandung, Jakarta]Mari kita bedah method dan pemanggilannya satu per satu:
- Baris 6-8 (
static <T> T pertama(List<T> daftar)):Tmuncul di dua tempat — tipe kembalian dan elemenList. Satu deklarasi<T>mengikat keduanya, sehingga method ini menjanjikan: “nilai yang dikembalikan bertipe sama dengan elemen daftar”. - Baris 11-18 (
static <T> List<T> dari(T... nilai)): varargsT... nilai) memakai type parameter, dan anotasi@SafeVarargs` menenangkan compiler terkait peringatan khusus varargs generik (kita bahas tuntas pada Chapter Type Erasure). - Baris 21-26 (
static <T> void tukar(...)):Tdipakai untuk variabel sementara dan operasiset, tanpa tipe kembalian — bukti type parameter tidak wajib muncul pada nilai balik. - Baris 29-34 (pemanggilan
pertama): pemanggilan pertama mengirimList<String>sehinggaT = String; pemanggilan kedua mengirimList<Integer>sehinggaT = 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:
InferensiSalah.java:10: error: incompatible types: inference variable T has incompatible bounds
equality constraints: Integer
upper bounds: String,ObjectPesan 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 #
- Oracle: Generic Methods (Java Tutorials)
- JLS SE25 §8.4.4: Generic Methods
- dev.java: Generics
- JLS SE25 §4.5: Type Arguments and Type Parameters
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AlatDaftar.java
Unduh berkasimport 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 berkasimport 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);
}
}