Generics membuat kode aman, tetapi versi awalnya membuat kode rewel: Map<String, List<Integer>> nilai = new HashMap<String, List<Integer>>() menulis informasi tipe yang sama dua kali pada satu baris. Java 7 memperkenalkan diamond operator <> agar type argument di sisi kanan disalin dari konteks di sisi kiri, dan Java 10 menambah var untuk variabel lokal yang tipe-nya pun dapat disimpulkan. Semua itu bertumpu pada satu mekanisme: type inference — proses compiler javac menyimpulkan type argument dari petunjuk di sekitarnya tanpa menurunkan satu pun pemeriksaan tipe. Pada Chapter ini kita jalankan diamond, var, target typing, dan inferensi berantai — lalu lihat batas tepat di mana kebiasaan inferensi mulai berisiko.
Diamond Operator #
Diamond menghilangkan penulisan ganda type argument pada konstruksi objek generik. Perhatikan dua gaya berikut pada program yang sama:
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InferensiDiamon {
// Target typing dari tipe kembalian: T pada ArrayList<> disalin dari List<String>
static List<String> buatNama() {
return new ArrayList<>();
}
public static void main(String[] args) {
// Gaya sebelum Java 7: type argument ditulis dua kali
Map<String, List<Integer>> gayaLama = new HashMap<String, List<Integer>>();
// Diamond sejak Java 7: type argument disalin dari variabel
Map<String, List<Integer>> gayaBaru = new HashMap<>();
// var sejak Java 10: tipe variabel pun disimpulkan dari kanan
var nama = new ArrayList<String>();
// Target typing pada generic method pustaka standar
List<String> kosong = Collections.emptyList();
gayaLama.put("Budi", List.of(80));
gayaBaru.put("Siti", List.of(90, 95));
nama.add("Dewi");
System.out.println("Lama : " + gayaLama);
System.out.println("Baru : " + gayaBaru);
System.out.println("var : " + nama);
System.out.println("Method : " + buatNama().getClass().getSimpleName());
System.out.println("Kosong : " + kosong.isEmpty());
}
}Output yang Dihasilkan:
Lama : {Budi=[80]}
Baru : {Siti=[90, 95]}
var : [Dewi]
Method : ArrayList
Kosong : trueMari kita bedah sumber inferensi pada tiap barisnya:
- Baris 15 (
new HashMap<String, List<Integer>>()) dan baris 18 (new HashMap<>()): keduanya menghasilkan objek dengan type argument identik. Diamond membaca tipe variabelgayaBarusebagai konteks, menyalinString, List<Integer>ke constructor, lalu memeriksa kesesuaian — hasil akhirnya sama persis dengan versi panjang, hanya tanpa pengulangan. - Baris 21 (
var nama = new ArrayList<String>()): arah inferensi terbalik dari diamond — type argumentStringtetap kita tulis di sisi kanan, danvarmenyimpulkan tipe variabelnya (ArrayList<String>). Setelah baris ini,nama.add("Dewi")sah dannama.add(42)ditolak compiler. - Baris 24 (
List<String> kosong = Collections.emptyList()): method generik tanpa argument apa pun. TipeTpada signature<T> List<T> emptyList()disimpulkan murni dari tipe variabel penerima — teknik target typing. - Baris 10 (
return new ArrayList<>()): target typing juga bekerja pada tipe kembalian method. Diamond menyalinStringdari deklarasiList<String>milikbuatNama.
var Bersama Generics #
var dan generics bekerja pada dua sisi assignment yang berbeda: generics parametrisasi tipe di sisi kanan, var menyimpulkan tipe di sisi kiri. Keduanya dapat dipadu (var nama = new ArrayList<String>()) atau dipakai terpisah. Batas pentingnya muncul saat var bertemu diamond polos:
Selain kasus tersebut, var mengikuti aturan yang ketat: tipe pasti ditentukan saat kompilasi (bukan saat runtime), tipe tersebut tercantum di berkas .class, dan IDE dapat menampilkannya. var tidak mengubah sifat statically typed Java — ia hanya memindahkan penulisan tipe dari tangan kita ke compiler.
Inferensi Berantai #
Kekuatan sesungguhnya tampak pada pemanggilan berantai, tempat hasil inferensi satu tahap menjadi petunjuk tahap berikutnya. Signature map pada Stream<T> adalah <R> Stream<R> map(Function<? super T, ? extends R> pemeta), dan type parameter R disimpulkan dari lambda yang kita tulis:
import java.util.List;
import java.util.Map;
public class RantaiInferensi {
public static void main(String[] args) {
// Inferensi mengalir sepanjang rantai pemanggilan
List<Integer> ganda = List.of(1, 2, 3, 4, 5)
.stream() // Stream<Integer> dari List<Integer>
.map(n -> n * 2) // type parameter R disimpulkan Integer
.toList(); // List<Integer>
System.out.println("Ganda: " + ganda);
Map<String, Integer> stok = Map.of("pensil", 12, "buku", 7);
int total = stok.entrySet()
.stream() // Stream<Map.Entry<String, Integer>>
.mapToInt(e -> e.getValue()) // IntStream
.sum();
System.out.println("Total: " + total);
}
}Output yang Dihasilkan:
Ganda: [2, 4, 6, 8, 10]
Total: 19Aliran inferensinya layak diikuti baris demi baris:
- Baris 7 (
List.of(1, 2, 3, 4, 5)): argument1..5menyimpulkanList<Integer>— inferensi generik paling dasar, dari argument literal. - Baris 8 (
.stream()):List<Integer>.stream()menghasilkanStream<Integer>tanpa type parameter baru — signature mengikutiCollection<E>. - Baris 9 (
.map(n -> n * 2)): lambda menerimaInteger, mengembalikann * 2bertipeInteger, sehinggaR = Integer. Tuliskann -> "teks"dan compiler menyimpulkanR = String— rantai berikutnya (.toList()) ikut berubah tipe, dan assignment keList<Integer>gagal dengan pesan inferensi yang telah kita kenal pada Chapter Generic Methods. - Baris 15-18 (
stok.entrySet().stream().mapToInt(...)): rantai panjang tanpa satu pun type argument tertulis.mapToIntmenghasilkanIntStream(bukanStream<Integer>) — spesialisasi primitive yang bebas boxing — dansum()mengembalikanint.
Keseimbangan Keterbacaan #
Inferensi memangkas kebisingan, tetapi tipe yang tidak tertulis tetap harus terbaca dari konteks. Dua pedoman praktis menjaga keseimbangan: pakai diamond untuk konstruktor yang type argument-nya sudah jelas dari variabel penerima, dan pertahankan type argument eksplisik pada API publik — parameter method dan tipe kembalian — karena pembaca kode di luar kita tidak melihat konteks assignment. Pedoman terakhir ini termasuk praktik merancang API generik yang kita rangkai pada Chapter penutup Section.
Sampai di sini semua perangkat generik telah lengkap: class, method, interface, bound, wildcard, erasure, inferensi. Masih tersisa satu kelompok materi berupa daftar larangan — hal-hal yang tidak dapat dilakukan generics karena erasure — yang kita susun sebagai peta rintangan pada Chapter Restrictions pada Generics.
Referensi #
- JLS SE25 §5.3: Assignment Contexts (target typing)
- JLS SE25 §18: Type Inference
- Java API: java.util.Collections.emptyList
- dev.java: Generics
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
InferensiDiamon.java
Unduh berkasimport java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InferensiDiamon {
// Target typing dari tipe kembalian: T pada ArrayList<> disalin dari List<String>
static List<String> buatNama() {
return new ArrayList<>();
}
public static void main(String[] args) {
// Gaya sebelum Java 7: type argument ditulis dua kali
Map<String, List<Integer>> gayaLama = new HashMap<String, List<Integer>>();
// Diamond sejak Java 7: type argument disalin dari variabel
Map<String, List<Integer>> gayaBaru = new HashMap<>();
// var sejak Java 10: tipe variabel pun disimpulkan dari kanan
var nama = new ArrayList<String>();
// Target typing pada generic method pustaka standar
List<String> kosong = Collections.emptyList();
gayaLama.put("Budi", List.of(80));
gayaBaru.put("Siti", List.of(90, 95));
nama.add("Dewi");
System.out.println("Lama : " + gayaLama);
System.out.println("Baru : " + gayaBaru);
System.out.println("var : " + nama);
System.out.println("Method : " + buatNama().getClass().getSimpleName());
System.out.println("Kosong : " + kosong.isEmpty());
}
}
RantaiInferensi.java
Unduh berkasimport java.util.List;
import java.util.Map;
public class RantaiInferensi {
public static void main(String[] args) {
// Inferensi mengalir sepanjang rantai pemanggilan
List<Integer> ganda = List.of(1, 2, 3, 4, 5)
.stream() // Stream<Integer> dari List<Integer>
.map(n -> n * 2) // type parameter R disimpulkan Integer
.toList(); // List<Integer>
System.out.println("Ganda: " + ganda);
Map<String, Integer> stok = Map.of("pensil", 12, "buku", 7);
int total = stok.entrySet()
.stream() // Stream<Map.Entry<String, Integer>>
.mapToInt(e -> e.getValue()) // IntStream
.sum();
System.out.println("Total: " + total);
}
}