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:

text
Lama   : {Budi=[80]}
Baru   : {Siti=[90, 95]}
var    : [Dewi]
Method : ArrayList
Kosong : true

Mari kita bedah sumber inferensi pada tiap barisnya:

  1. Baris 15 (new HashMap<String, List<Integer>>()) dan baris 18 (new HashMap<>()): keduanya menghasilkan objek dengan type argument identik. Diamond membaca tipe variabel gayaBaru sebagai konteks, menyalin String, List<Integer> ke constructor, lalu memeriksa kesesuaian — hasil akhirnya sama persis dengan versi panjang, hanya tanpa pengulangan.
  2. Baris 21 (var nama = new ArrayList<String>()): arah inferensi terbalik dari diamond — type argument String tetap kita tulis di sisi kanan, dan var menyimpulkan tipe variabelnya (ArrayList<String>). Setelah baris ini, nama.add("Dewi") sah dan nama.add(42) ditolak compiler.
  3. Baris 24 (List<String> kosong = Collections.emptyList()): method generik tanpa argument apa pun. Tipe T pada signature <T> List<T> emptyList() disimpulkan murni dari tipe variabel penerima — teknik target typing.
  4. Baris 10 (return new ArrayList<>()): target typing juga bekerja pada tipe kembalian method. Diamond menyalin String dari deklarasi List<String> milik buatNama.

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:

text
Ganda: [2, 4, 6, 8, 10]
Total: 19

Aliran inferensinya layak diikuti baris demi baris:

  1. Baris 7 (List.of(1, 2, 3, 4, 5)): argument 1..5 menyimpulkan List<Integer> — inferensi generik paling dasar, dari argument literal.
  2. Baris 8 (.stream()): List<Integer>.stream() menghasilkan Stream<Integer> tanpa type parameter baru — signature mengikuti Collection<E>.
  3. Baris 9 (.map(n -> n * 2)): lambda menerima Integer, mengembalikan n * 2 bertipe Integer, sehingga R = Integer. Tuliskan n -> "teks" dan compiler menyimpulkan R = String — rantai berikutnya (.toList()) ikut berubah tipe, dan assignment ke List<Integer> gagal dengan pesan inferensi yang telah kita kenal pada Chapter Generic Methods.
  4. Baris 15-18 (stok.entrySet().stream().mapToInt(...)): rantai panjang tanpa satu pun type argument tertulis. mapToInt menghasilkan IntStream (bukan Stream<Integer>) — spesialisasi primitive yang bebas boxing — dan sum() mengembalikan int.

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 #

Berkas Sumber

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

InferensiDiamon.java

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

RantaiInferensi.java

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