Kadang kita membutuhkan duplikat objek: misalnya menyimpan snapshot rekening sebelum diproses, atau menguji perubahan pada salinan tanpa mengganggu data asli. Java menyediakan jalur bawaan lewat method clone() yang diwariskan Object, tetapi jalur tersebut penuh jebakan — dari mekanisme Cloneable yang aneh hingga shallow copy yang diam-diam membagikan field mutable antara asli dan salinan. Praktik industri modern justru menjauhi clone() dan memakai copy constructor atau copy factory. Pada Chapter ini kita buktikan dulu jebakan salinan dangkal dengan program yang berjalan, lalu bangun alternatif yang aman dan mudah dikontrol.
Mekanisme clone dan Cloneable #
Method clone() di java.lang.Object bersifat protected, sehingga kode luar tidak bisa memanggilnya begitu saja — class kita harus meng-override-nya menjadi public terlebih dahulu. Ada satu syarat lagi yang tidak lazim: class tersebut harus mengimplementasikan interface penanda Cloneable. Interface ini kosong tanpa method; perannya hanya sebagai izin — jika super.clone() dipanggil pada class tanpa Cloneable, JVM melempar CloneNotSupportedException saat runtime, bukan saat kompilasi.
Ketika syarat terpenuhi, super.clone() membuat salinan bit demi bit dari objek: setiap field disalin nilainya ke objek baru. Untuk field primitif seperti double, penyalinan nilai memang sudah cukup. Namun untuk field referensi seperti List, yang disalin hanyalah alamatnya — asli dan salinan kini menunjuk objek List yang sama. Inilah shallow copy, dan inilah sumber masalah utamanya.
Jebakan Salinan Dangkal #
Program berikut menyalin rekening dengan clone(), lalu mencatat transaksi baru lewat salinan. Field mutasi bertipe List<String> — perhatikan ke mana transaksi tersebut benar-benar pergi:
import java.util.ArrayList;
import java.util.List;
class Rekening implements Cloneable {
private final String nomor;
private final List<String> mutasi;
public Rekening(String nomor) {
this.nomor = nomor;
this.mutasi = new ArrayList<>();
}
public void catat(String transaksi) {
mutasi.add(transaksi);
}
public List<String> mutasi() {
return mutasi;
}
// clone() di Object bersifat protected; override menjadi public
// agar pemanggil luar boleh memakainya.
@Override
public Rekening clone() {
try {
return (Rekening) super.clone(); // Object.clone: salinan DANGKAL
} catch (CloneNotSupportedException e) {
// Tidak terjadi karena class ini implements Cloneable.
throw new AssertionError(e);
}
}
}
public class SalinanDangkal {
public static void main(String[] args) {
Rekening asli = new Rekening("123-456");
asli.catat("setor 500000");
Rekening salinan = asli.clone();
salinan.catat("tarik 100000"); // dicatat lewat SALINAN...
// ...tetapi muncul juga di ASLI: bukti field dibagi bersama.
System.out.println("mutasi asli : " + asli.mutasi());
System.out.println("mutasi salinan: " + salinan.mutasi());
}
}Output yang Dihasilkan:
mutasi asli : [setor 500000, tarik 100000]
mutasi salinan: [setor 500000, tarik 100000]Output tersebut adalah bukti kebocoran: transaksi tarik 100000 dicatat lewat salinan, tetapi muncul di mutasi milik asli juga. Mari kita bedah mengapa:
class Rekening implements Cloneable: tanpa interface penanda ini, pemanggilansuper.clone()di runtime melemparCloneNotSupportedException— syarat yang diperiksa JVM, bukan compiler.@Override public Rekening clone(): akses dinaikkan dariprotectedkepublicsupaya bisa dipanggil dari luar class, dan tipe kembalian dipersempit keRekening(covariant return type) supaya pemanggil tidak perlu meng-cast.return (Rekening) super.clone();: JVM menyalin field satu per satu. Fieldnomor(referensiString) berbagi instance yang sama — aman, karenaStringimmutable. Fieldmutasijuga berbagi instance yang sama — danArrayListmutable, sehingga perubahan dari satu sisi terlihat di sisi lain.salinan.catat("tarik 100000"): method ini menambah elemen keArrayListyang sama. Kedua objek rekening menampilkan daftar identik[setor 500000, tarik 100000].
Solusi dari clone() terhadap masalah ini adalah salinan dalam (deep copy) manual di dalam override — membangun ulang setiap field mutable satu per satu, atau memanggil clone() berantai sampai ke bawah. Pada hierarki objek yang dalam, kode tersebut cepat membengkak dan rapuh terhadap field baru yang lupa disalin.
Copy Constructor sebagai Alternatif #
Copy constructor adalah constructor biasa yang menerima parameter bertipe class itu sendiri. Seluruh keputusan penyalinan hidup di satu tempat yang terlihat jelas: field primitif disalin langsung, field mutable dibangun ulang sebagai objek baru:
import java.util.ArrayList;
import java.util.List;
class Rekening {
private final String nomor;
private final List<String> mutasi;
public Rekening(String nomor) {
this.nomor = nomor;
this.mutasi = new ArrayList<>();
}
// Copy constructor: penyalinan di satu tempat dengan kontrol penuh.
public Rekening(Rekening lain) {
this.nomor = lain.nomor;
this.mutasi = new ArrayList<>(lain.mutasi); // salinan dalam (deep copy)
}
public void catat(String transaksi) {
mutasi.add(transaksi);
}
public List<String> mutasi() {
return mutasi;
}
}
public class KonstruktorSalin {
public static void main(String[] args) {
Rekening asli = new Rekening("123-456");
asli.catat("setor 500000");
Rekening salinan = new Rekening(asli);
salinan.catat("tarik 100000");
// Mutasi salinan tidak lagi merambat ke asli.
System.out.println("mutasi asli : " + asli.mutasi());
System.out.println("mutasi salinan: " + salinan.mutasi());
}
}Output yang Dihasilkan:
mutasi asli : [setor 500000]
mutasi salinan: [setor 500000, tarik 100000]Hasilnya kini sehat: asli tetap berisi [setor 500000] sementara salinan berisi [setor 500000, tarik 100000]. Constructor Rekening(Rekening lain) menyalin isi mutasi ke ArrayList baru lewat new ArrayList<>(lain.mutasi), sehingga kedua rekening memegang daftar terpisah. Tidak ada interface penanda, tidak ada exception yang harus ditangkap, tidak ada casting — hanya constructor biasa yang mudah dibaca dan di-debug.
clone() tetap sah dipakai — pustaka standar sendiri memakainya pada ArrayList — tetapi setiap field mutable harus disalin sadar dan teruji. Untuk class buatan sendiri, copy constructor memberi kontrol yang sama tanpa mekanisme Cloneable. Pertanyaan berikutnya lebih radikal: bagaimana kalau objek memang tidak boleh berubah sejak diciptakan? Chapter Immutability merancang class yang menutup pintu mutasi sejak awal.
Referensi #
- Oracle API: java.lang.Object (clone)
- Oracle API: java.lang.Cloneable
- Oracle API: java.util.ArrayList (copy constructor)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KonstruktorSalin.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
class Rekening {
private final String nomor;
private final List<String> mutasi;
public Rekening(String nomor) {
this.nomor = nomor;
this.mutasi = new ArrayList<>();
}
// Copy constructor: penyalinan di satu tempat dengan kontrol penuh.
public Rekening(Rekening lain) {
this.nomor = lain.nomor;
this.mutasi = new ArrayList<>(lain.mutasi); // salinan dalam (deep copy)
}
public void catat(String transaksi) {
mutasi.add(transaksi);
}
public List<String> mutasi() {
return mutasi;
}
}
public class KonstruktorSalin {
public static void main(String[] args) {
Rekening asli = new Rekening("123-456");
asli.catat("setor 500000");
Rekening salinan = new Rekening(asli);
salinan.catat("tarik 100000");
// Mutasi salinan tidak lagi merambat ke asli.
System.out.println("mutasi asli : " + asli.mutasi());
System.out.println("mutasi salinan: " + salinan.mutasi());
}
}
SalinanDangkal.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
class Rekening implements Cloneable {
private final String nomor;
private final List<String> mutasi;
public Rekening(String nomor) {
this.nomor = nomor;
this.mutasi = new ArrayList<>();
}
public void catat(String transaksi) {
mutasi.add(transaksi);
}
public List<String> mutasi() {
return mutasi;
}
// clone() di Object bersifat protected; override menjadi public
// agar pemanggil luar boleh memakainya.
@Override
public Rekening clone() {
try {
return (Rekening) super.clone(); // Object.clone: salinan DANGKAL
} catch (CloneNotSupportedException e) {
// Tidak terjadi karena class ini implements Cloneable.
throw new AssertionError(e);
}
}
}
public class SalinanDangkal {
public static void main(String[] args) {
Rekening asli = new Rekening("123-456");
asli.catat("setor 500000");
Rekening salinan = asli.clone();
salinan.catat("tarik 100000"); // dicatat lewat SALINAN...
// ...tetapi muncul juga di ASLI: bukti field dibagi bersama.
System.out.println("mutasi asli : " + asli.mutasi());
System.out.println("mutasi salinan: " + salinan.mutasi());
}
}