Semua collection yang kita pakai sejauh ini — ArrayList, HashMap, TreeSet — bersifat mutable: siapa pun yang memegang referensinya boleh menambah, menghapus, atau mengubah isinya kapan saja. Kelenturan itu berguna saat membangun data, tetapi berbahaya saat data dikirim ke luar: method pembaca bisa saja tanpa sengaja mengosongkan daftar yang kita pinjamkan, dan bug semacam itu sulit dilacak karena tidak meninggalkan jejak di tempat kejadian. Java 9 menjawabnya dengan pabrik pembuat immutable collection: List.of, Set.of, dan Map.of — sekali dibentuk, isinya terkunci permanen. Pada Chapter ini kita buktikan sendiri bahwa seluruh operasi tulis ditolak, mengapa null ditolak sejak pembuatan, dan beda mendasar antara collection yang benar-benar beku dengan unmodifiable view dari era sebelumnya.
Pabrik List.of, Set.of, Map.of #
Ketiga method ini adalah static factory yang menerima elemen langsung sebagai argumen: List.of("Beras", "Gula"), Set.of("Budi", "Siti"), dan Map.of("Pensil", 120, "Buku", 45) — pasangan key-value berturut-turut. Ada overloads hingga sepuluh argumen; untuk lebih dari sepuluh pasangan, Map menyediakan Map.ofEntries(Map.entry("Pensil", 120), ...) yang lebih rapi. Method List.copyOf, Set.copyOf, dan Map.copyOf melengkapi keluarga ini dengan cara mengambil salinan beku dari collection yang sudah ada.
Pabrik-pabrik ini memvalidasi masukan sejak pembuatan. List.of menerima elemen duplikat (list memang boleh), tetapi Set.of dan Map.of menolaknya dengan IllegalArgumentException — pesan aslinya terverifikasi: duplicate element: a untuk Set.of("a", "b", "a") dan duplicate key: a untuk Map.of("a", 1, "a", 2). Validasi di muka ini berarti collection lahir dalam keadaan valid: set yang dibuat pasti tanpa duplikat, tanpa perlu pemeriksaan tambahan.
Semua Operasi Tulis Ditolak #
Janji immutable diuji langsung: program berikut mencoba lima operasi tulis berbeda pada List.of, Set.of, dan Map.of, lalu menangkap exception yang muncul:
import java.util.*;
public class TolakanPerubahan {
public static void main(String[] args) {
List<String> belanja = List.of("Beras", "Gula", "Kopi");
Set<String> tim = Set.of("Budi", "Siti", "Dewi");
Map<String, Integer> stok = Map.of("Pensil", 120, "Buku", 45);
System.out.println("belanja : " + belanja);
System.out.println("tim : size " + tim.size()
+ ", contains(Siti) = " + tim.contains("Siti"));
System.out.println("stok : get(Pensil) = " + stok.get("Pensil")
+ ", size " + stok.size());
uji("belanja.add ", () -> belanja.add("Teh"));
uji("belanja.remove", () -> belanja.remove("Gula"));
uji("belanja.set ", () -> belanja.set(0, "Tepung"));
uji("tim.add ", () -> tim.add("Anton"));
uji("stok.put ", () -> stok.put("Spidol", 30));
}
static void uji(String label, Runnable aksi) {
try {
aksi.run();
System.out.println(label + ": BERHASIL (tidak seharusnya)");
} catch (UnsupportedOperationException e) {
System.out.println(label + ": " + e);
}
}
}Output yang Dihasilkan:
belanja : [Beras, Gula, Kopi]
tim : size 3, contains(Siti) = true
stok : get(Pensil) = 120, size 2
belanja.add : java.lang.UnsupportedOperationException
belanja.remove: java.lang.UnsupportedOperationException
belanja.set : java.lang.UnsupportedOperationException
tim.add : java.lang.UnsupportedOperationException
stok.put : java.lang.UnsupportedOperationExceptionPoin bedahnya:
- Baris 6-8 (tiga pembuatan): seluruh operasi baca —
size,contains,get, iterasi,forEach— berjalan normal; yang terkunci hanyalah operasi tulis. - Baris 13-17 (
uji(...)): helper menerimaRunnablesehingga tiap percobaan ditulis satu baris; kelima operasi menghasilkanjava.lang.UnsupportedOperationExceptiontanpa pesan tambahan. - Baris 23-29 (blok
catch): exception dilempar oleh implementasiList.of/Set.of/Map.of(class internalImmutableCollections), bukan oleh compiler — pengubahan isi adalah pelanggaran runtime, sehingga penangkapanUnsupportedOperationExceptiontetap sah bila kita sengaja mengujinya.
Elemen null Ditolak Sejak Pembuatan #
Aturan kedua yang membedakan pabrik immutable dari ArrayList lama: null tidak diperbolehkan — sebagai elemen List/Set maupun sebagai key/value Map. Berkas berikut sengaja menyelipkan satu null:
import java.util.*;
public class NullDitolak {
public static void main(String[] args) {
System.out.println("List.of dengan elemen biasa berhasil dibuat.");
// Elemen null ditolak sejak pembuatan collection immutable.
List<String> belanja = List.of("Beras", null);
System.out.println("Baris ini tidak pernah tercetak.");
}
}JVM menghentikan program dengan exception:
Exception in thread "main" java.lang.NullPointerExceptionStack trace menunjuk ke java.util.Objects.requireNonNull yang dipanggil dari dalam java.util.List.of — pemeriksaan terjadi di constructor collection, bukan saat elemennya dipakai. Kebijakan ini disengaja: collection yang menjanjikan dirinya tak berubah sekaligus menjamin “tidak ada elemen null yang menyusup” menghapus dua sumber NullPointerException paling umum sekaligus. Karena null ditolak, contains(null) pun mengembalikan false alih-alih berperilaku khusus seperti pada HashSet.
Immutable Berbeda dari Unmodifiable View #
Jauh sebelum List.of ada, Java sudah punya Collections.unmodifiableList(list) — dan keduanya sering dianggap sama padahal berbeda. Method warisan itu menghasilkan view read-only yang masih menempel pada list aslinya: ketika list asli berubah, view ikut berubah. Program berikut mempertaruhkan keduanya berdampingan:
import java.util.*;
public class ImmutableVsUnmodifiable {
public static void main(String[] args) {
List<String> asli = new ArrayList<>(List.of("Beras", "Gula"));
// View unmodifiable: read-only, tetapi masih menempel pada list asli.
List<String> view = Collections.unmodifiableList(asli);
// Salinan immutable: terputus total dari list asli.
List<String> beku = List.copyOf(asli);
asli.add("Kopi"); // mutasi pada list asli yang masih mutable
System.out.println("asli : " + asli);
System.out.println("view : " + view);
System.out.println("beku : " + beku);
// View read-only tetap menolak modifikasi langsung.
try {
view.add("Teh");
} catch (UnsupportedOperationException e) {
System.out.println("view.add ditolak : " + e);
}
}
}Output yang Dihasilkan:
asli : [Beras, Gula, Kopi]
view : [Beras, Gula, Kopi]
beku : [Beras, Gula]
view.add ditolak : java.lang.UnsupportedOperationExceptionPoin bedahnya:
- Baris 7 (
Collections.unmodifiableList(asli)): view membungkus referensi yang sama; operasi baca diteruskan ke list asli, operasi tulis ditolak denganUnsupportedOperationException— sama seperti immutable dari luar. - Baris 10 (
List.copyOf(asli)): method ini memeriksa asal salinan; bila sumbernya sudah immutable, objek yang sama dikembalikan ulang, dan bila tidak, salinan beku dibuat. Hasilnya terputus dariasliselamanya. - Baris 13 (
asli.add("Kopi")): pemilik list asli masih berhak mengubah — danviewikut menampilkanKopi, sementarabekutetap[Beras, Gula]. Kesimpulannya tegas: unmodifiable berarti “tanganmu terikat”, sedangkan immutable berarti “isinya memang tidak bisa berubah”.
Dengan collection immutable, kita memperoleh data yang bisa dibagikan tanpa rasa cemas: tidak ada pemanggil yang bisa mengubah isi, tidak ada null yang menyusup, dan tidak ada aliasing tersembunyi. Mesin pengubahan yang tersisa adalah utilitas satu pintu — class Collections dan Arrays dengan puluhan operasi siap pakai. Chapter berikutnya merangkum operasi-operasi itu dari sort hingga shuffle.
Referensi #
- Java API Specification: java.util.List
- Java API Specification: java.util.Map
- Java API Specification: java.util.Collections
- Dev.java: The Java Collections Framework
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ImmutableVsUnmodifiable.java
Unduh berkasimport java.util.*;
public class ImmutableVsUnmodifiable {
public static void main(String[] args) {
List<String> asli = new ArrayList<>(List.of("Beras", "Gula"));
// View unmodifiable: read-only, tetapi masih menempel pada list asli.
List<String> view = Collections.unmodifiableList(asli);
// Salinan immutable: terputus total dari list asli.
List<String> beku = List.copyOf(asli);
asli.add("Kopi"); // mutasi pada list asli yang masih mutable
System.out.println("asli : " + asli);
System.out.println("view : " + view);
System.out.println("beku : " + beku);
// View read-only tetap menolak modifikasi langsung.
try {
view.add("Teh");
} catch (UnsupportedOperationException e) {
System.out.println("view.add ditolak : " + e);
}
}
}
NullDitolak.java
Unduh berkasimport java.util.*;
public class NullDitolak {
public static void main(String[] args) {
System.out.println("List.of dengan elemen biasa berhasil dibuat.");
// Elemen null ditolak sejak pembuatan collection immutable.
List<String> belanja = List.of("Beras", null);
System.out.println("Baris ini tidak pernah tercetak.");
}
}
TolakanPerubahan.java
Unduh berkasimport java.util.*;
public class TolakanPerubahan {
public static void main(String[] args) {
List<String> belanja = List.of("Beras", "Gula", "Kopi");
Set<String> tim = Set.of("Budi", "Siti", "Dewi");
Map<String, Integer> stok = Map.of("Pensil", 120, "Buku", 45);
System.out.println("belanja : " + belanja);
System.out.println("tim : size " + tim.size()
+ ", contains(Siti) = " + tim.contains("Siti"));
System.out.println("stok : get(Pensil) = " + stok.get("Pensil")
+ ", size " + stok.size());
uji("belanja.add ", () -> belanja.add("Teh"));
uji("belanja.remove", () -> belanja.remove("Gula"));
uji("belanja.set ", () -> belanja.set(0, "Tepung"));
uji("tim.add ", () -> tim.add("Anton"));
uji("stok.put ", () -> stok.put("Spidol", 30));
}
static void uji(String label, Runnable aksi) {
try {
aksi.run();
System.out.println(label + ": BERHASIL (tidak seharusnya)");
} catch (UnsupportedOperationException e) {
System.out.println(label + ": " + e);
}
}
}