Chapter Object Cloning menutup dengan pertanyaan menarik: bagaimana kalau objek memang tidak boleh berubah sejak diciptakan? Objek yang bisa berubah (mutable) menghadirkan risiko diam-diam — siapa pun yang memegang referensinya bisa mengubah isinya, termasuk lewat pintu yang tidak kita sadari terbuka. Immutability menutup semua pintu itu sekaligus: class yang dirancang agar state-nya beku setelah constructor selesai. Hasilnya bukan sekadar gaya kode — objek immutable bebas dibagikan antar variabel, antar method, bahkan antar thread tanpa saling mengganggu. Pada Chapter ini kita lihat dulu kebocoran aliasing pada class mutable, lalu bangun class immutable dengan resep yang lengkap.

Masalah Objek yang Bisa Berubah #

Bayangkan class Buku yang menyimpan daftar penulis. Kedua arah data berpotensi bocor: referensi List dari pemanggil masuk ke constructor tanpa disalin, dan getter mengembalikan referensi internal apa adanya. Objek dan list luar kini saling mengintip:

java BahayaAlias.java
import java.util.ArrayList;
import java.util.List;

class Buku {
    // Versi mutable: referensi list dari luar tersimpan apa adanya.
    private final String judul;
    private final List<String> penulis;

    public Buku(String judul, List<String> penulis) {
        this.judul = judul;
        this.penulis = penulis; // referensi luar masuk tanpa disalin
    }

    public List<String> penulis() {
        return penulis; // referensi internal bocor ke luar
    }
}

public class BahayaAlias {
    public static void main(String[] args) {
        List<String> namaPenulis = new ArrayList<>();
        namaPenulis.add("Budi Santoso");

        Buku buku = new Buku("Java Dasar", namaPenulis);

        // Mutasi list luar...
        namaPenulis.add("Siti Rahayu");

        // ...muncul di dalam objek: efek aliasing.
        System.out.println("penulis buku: " + buku.penulis());

        // Bocor dua arah: mutasi lewat getter ikut mengubah list luar.
        buku.penulis().clear();
        System.out.println("list luar setelah clear: " + namaPenulis);
    }
}

Output yang Dihasilkan:

text
penulis buku: [Budi Santoso, Siti Rahayu]
list luar setelah clear: []

Dua baris output itu adalah pembuktian aliasing dua arah. Penambahan "Siti Rahayu" pada namaPenulis muncul di dalam buku karena constructor menyimpan referensi list yang sama. Sebaliknya, buku.penulis().clear() ikut mengosongkan namaPenulis milik main karena getter menyerahkan referensi internal. Objek Buku tampak berperilaku berubah tanpa method-nya menyentuh field — inilah sumber bug yang sulit dilacak pada kode besar.

Resep Class Immutable #

Class immutable dibangun dari lima keputusan yang saling menguatkan:

  • Class final: mencegah subclass menambahkan setter atau melonggarkan aturan beku.
  • Semua field private final: nilai diisi sekali di constructor dan tidak bisa ditulis ulang.
  • Tanpa setter: tidak ada method yang mengubah state; hanya accessor pembaca.
  • Defensive copy dua arah untuk field mutable: salin saat data masuk lewat constructor, dan salin (atau kembalikan salinan beku) saat data keluar lewat getter.
  • Perubahan = objek baru: method semacam denganJudul(...) mengembalikan instance baru alih-alih mengubah instance lama.

Field String, primitif, dan class immutable lain tidak butuh defensive copy karena isinya memang tidak bisa diubah. Yang wajib disalin hanyalah tipe mutable seperti ArrayList — persis daftar penulis pada contoh kita.

Implementasi Class Immutable #

Resep tersebut diterapkan penuh pada BukuTetap. Constructor menyalin daftar yang masuk, getter mengembalikan salinan beku List.copyOf(...), dan “perubahan” judul menghasilkan objek baru:

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

// Class final: tidak bisa diwarisi lalu dilonggarkan aturannya.
final class BukuTetap {
    // Semua field private final: hanya diisi di constructor.
    private final String judul;
    private final List<String> penulis;

    public BukuTetap(String judul, List<String> penulis) {
        this.judul = judul;
        this.penulis = new ArrayList<>(penulis); // defensive copy saat masuk
    }

    public String judul() {
        return judul;
    }

    public List<String> penulis() {
        return List.copyOf(penulis); // defensive copy saat keluar
    }

    // "Perubahan" berarti objek baru; objek lama tidak pernah berubah.
    public BukuTetap denganJudul(String judulBaru) {
        return new BukuTetap(judulBaru, penulis);
    }
}

public class KelasImmutable {
    public static void main(String[] args) {
        List<String> namaPenulis = new ArrayList<>();
        namaPenulis.add("Budi Santoso");

        BukuTetap buku = new BukuTetap("Java Dasar", namaPenulis);

        // Mutasi list luar tidak merambat ke dalam objek.
        namaPenulis.add("Siti Rahayu");
        System.out.println("penulis buku : " + buku.penulis());
        System.out.println("list luar    : " + namaPenulis);

        // "Mengubah" judul menghasilkan objek baru.
        BukuTetap edisiBaru = buku.denganJudul("Java Dasar Edisi 2");
        System.out.println("judul lama   : " + buku.judul());
        System.out.println("judul baru   : " + edisiBaru.judul());
    }
}

Output yang Dihasilkan:

text
penulis buku : [Budi Santoso]
list luar    : [Budi Santoso, Siti Rahayu]
judul lama   : Java Dasar
judul baru   : Java Dasar Edisi 2

Mari kita bedah perisainya satu per satu:

  1. final class BukuTetap: modifier final pada class menutup jalur pewarisan. Tanpa ini, subclass bisa menambah method yang mengubah state dan seluruh janji kebeku batal.
  2. this.penulis = new ArrayList<>(penulis): constructor membuat list internal sendiri. namaPenulis.add("Siti Rahayu") pada output terbukti tidak merambat — buku.penulis() tetap [Budi Santoso].
  3. return List.copyOf(penulis): List.copyOf (Java 10+) mengembalikan list beku yang menolak operasi tambah/hapus. Pemanggil mendapat data untuk dibaca, bukan pintu untuk mengubah.
  4. denganJudul(...): alih-alih menimpa field judul, method ini membangun BukuTetap baru. Output menunjukkan buku lama tetap "Java Dasar" sementara edisiBaru membawa "Java Dasar Edisi 2" — keduanya hidup berdampingan tanpa saling mengganggu.

Keuntungan Thread-Safe dan Bebas Aliasing #

Keuntungan pertama adalah kebebasan berbagi. Objek immutable boleh diteruskan ke method mana pun, disimpan di field mana pun, atau dipublikasikan ke thread lain tanpa synchronized — tidak ada yang bisa mengubahnya, jadi tidak ada kondisi balapan atas isinya. Karena state tidak pernah berubah, hashCode-nya pun stabil, menjadikannya key HashMap yang aman. JDK sendiri mempraktikkan hal ini pada String, Integer, dan java.time.LocalDate.

Keuntungan kedua adalah kejelasan penelusuran. Pada class mutable, memahami sebuah objek berarti mengawasi semua kode yang pernah memegang referensinya. Pada class immutable, jawabannya pasti: isinya sama seperti saat dibuat. Bug “data berubah sendiri” seperti pada BahayaAlias menjadi mustahil secara konstruksi.

Menulis class immutable manual memang menuntut disiplin — lima aturan di atas harus dipatuhi tanpa kecuali. Kabar baiknya, Java 16 memperkenalkan record yang otomatis menerapkan sebagian besar resep tersebut: field final, accessor, equals, hashCode, dan toString dalam satu deklarasi ringkas. Chapter Records membahasnya tuntas.

Referensi #

Berkas Sumber

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

BahayaAlias.java

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

class Buku {
    // Versi mutable: referensi list dari luar tersimpan apa adanya.
    private final String judul;
    private final List<String> penulis;

    public Buku(String judul, List<String> penulis) {
        this.judul = judul;
        this.penulis = penulis; // referensi luar masuk tanpa disalin
    }

    public List<String> penulis() {
        return penulis; // referensi internal bocor ke luar
    }
}

public class BahayaAlias {
    public static void main(String[] args) {
        List<String> namaPenulis = new ArrayList<>();
        namaPenulis.add("Budi Santoso");

        Buku buku = new Buku("Java Dasar", namaPenulis);

        // Mutasi list luar...
        namaPenulis.add("Siti Rahayu");

        // ...muncul di dalam objek: efek aliasing.
        System.out.println("penulis buku: " + buku.penulis());

        // Bocor dua arah: mutasi lewat getter ikut mengubah list luar.
        buku.penulis().clear();
        System.out.println("list luar setelah clear: " + namaPenulis);
    }
}

KelasImmutable.java

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

// Class final: tidak bisa diwarisi lalu dilonggarkan aturannya.
final class BukuTetap {
    // Semua field private final: hanya diisi di constructor.
    private final String judul;
    private final List<String> penulis;

    public BukuTetap(String judul, List<String> penulis) {
        this.judul = judul;
        this.penulis = new ArrayList<>(penulis); // defensive copy saat masuk
    }

    public String judul() {
        return judul;
    }

    public List<String> penulis() {
        return List.copyOf(penulis); // defensive copy saat keluar
    }

    // "Perubahan" berarti objek baru; objek lama tidak pernah berubah.
    public BukuTetap denganJudul(String judulBaru) {
        return new BukuTetap(judulBaru, penulis);
    }
}

public class KelasImmutable {
    public static void main(String[] args) {
        List<String> namaPenulis = new ArrayList<>();
        namaPenulis.add("Budi Santoso");

        BukuTetap buku = new BukuTetap("Java Dasar", namaPenulis);

        // Mutasi list luar tidak merambat ke dalam objek.
        namaPenulis.add("Siti Rahayu");
        System.out.println("penulis buku : " + buku.penulis());
        System.out.println("list luar    : " + namaPenulis);

        // "Mengubah" judul menghasilkan objek baru.
        BukuTetap edisiBaru = buku.denganJudul("Java Dasar Edisi 2");
        System.out.println("judul lama   : " + buku.judul());
        System.out.println("judul baru   : " + edisiBaru.judul());
    }
}