Klaim yang sering terdengar di forum diskusi — “Java mengirim objek by reference” — perlu kita uji sendiri sebelum percaya. Kenyataannya, Java selalu pass by value: setiap argument dievaluasi menjadi satu nilai, lalu nilai itulah yang disalin ke parameter method. Bedanya hanya pada jenis nilai yang disalin: untuk tipe primitive, yang disalin adalah isi angkanya; untuk tipe reference, yang disalin adalah alamat objek di heap — bukan objeknya. Konsekuensi kedua jenis salinan tersebut justru berlawanan, dan pada Chapter ini kita buktikan keduanya lewat dua percobaan klasik: method swap yang gagal menukar variabel pemanggil, dan method yang berhasil mengubah field objek.
Java Selalu Pass by Value #
Mari kita tetapkan dulu mekanismenya. Ketika pemanggilan tukar(x, y) dievaluasi, JVM mengambil nilai x dan y, lalu mengisi parameter a dan b di stack frame method dengan salinan nilai-nilai tersebut. Parameter dan variabel pemanggil adalah dua lokasi memori yang berbeda; pernyataan apa pun yang menimpa parameter hanya mengubah salinannya. Ketika method selesai, stack frame dibongkar bersama seluruh parameternya — variabel x dan y di pemanggil tidak pernah tersentuh.
Untuk tipe reference, ceritanya sama persis pada tahap penyalinan. Variabel Rekening tabungan tidak memuat objek, melainkan alamat objek Rekening yang dibuat new di heap. Yang disalin ke parameter adalah alamat tersebut, sehingga parameter dan variabel pemanggil menunjuk ke objek yang sama. Inilah asal dua perilaku berlawanan tadi: menimpa parameter berarti memutus hubungan salinan saja, sedangkan menelusuri parameter ke objek lalu mengubah field-nya mengubah objek yang sama-sama ditunjuk.
Swap yang Gagal #
Percobaan pertama adalah uji paling terkenal untuk pass by value: menukar isi dua variabel int lewat method:
public class TukarPrimitif {
static void tukar(int a, int b) {
int sementara = a;
a = b;
b = sementara;
System.out.println("di dalam tukar: a = " + a + ", b = " + b);
}
public static void main(String[] args) {
int x = 1;
int y = 9;
System.out.println("sebelum : x = " + x + ", y = " + y);
tukar(x, y);
System.out.println("sesudah : x = " + x + ", y = " + y);
}
}Output yang Dihasilkan:
sebelum : x = 1, y = 9
di dalam tukar: a = 9, b = 1
sesudah : x = 1, y = 9Hasil eksekusinya menunjukkan pertukaran yang berhasil di dalam method dan batal di luar: baris kedua mencatat a = 9, b = 1, tetapi baris ketiga tetap mencatat x = 1, y = 9. Mari kita bedah alasannya:
- Baris 19 (
tukar(x, y);): nilai1dan9disalin ke parameteradanbdi stack frame method. Variabelxdanytetap di framemaindengan nilainya masing-masing. - Baris 3-6 (
int sementara = a; a = b; b = sementara;): tiga pernyataan ini menukar isi parameteradanb— dua salinan di frame method. Rangkaian penukaran tidak pernah menyentuh variabelxdany. - Baris 20 (
System.out.println("sesudah ...")): frametukarsudah dibongkar, parameteradanbikut terbuang bersama frame-nya. Variabelxdanytidak pernah diubah, sehingga tercetak nilai aslinya.
Salinan Referensi Bukan Salinan Objek #
Sekarang bagian yang membuat banyak pembelajar salah paham. Kita siapkan class Rekening sederhana sebagai berkas definisi terpisah:
public class Rekening {
String pemilik;
double saldo;
Rekening(String pemilik, double saldo) {
this.pemilik = pemilik;
this.saldo = saldo;
}
}Program berikut memanggil dua method dengan peran yang bertolak belakang: isiSaldo menelusuri referensi lalu mengubah field objek, sedangkan gantiRekening menimpa parameter dengan objek baru:
public class MutasiObjek {
// Mutasi field lewat salinan referensi: perubahan terlihat pemanggil
static void isiSaldo(Rekening rekening, double jumlah) {
rekening.saldo = rekening.saldo + jumlah;
}
// Mengganti referensi parameter ke objek baru: TIDAK terlihat pemanggil
static void gantiRekening(Rekening rekening) {
rekening = new Rekening("Siti", 999999);
rekening.saldo = 0;
}
public static void main(String[] args) {
Rekening tabungan = new Rekening("Budi", 100000);
System.out.println("awal : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
isiSaldo(tabungan, 50000);
System.out.println("setelah isi : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
gantiRekening(tabungan);
System.out.println("setelah ganti : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
}
}Output yang Dihasilkan:
awal : Budi - Rp100000.0
setelah isi : Budi - Rp150000.0
setelah ganti : Budi - Rp150000.0Dua baris terakhir hasil eksekusi adalah inti Chapter ini. Setelah isiSaldo(tabungan, 50000), saldo yang terbaca pemanggil naik menjadi 150000.0 — padahal method hanya memegang salinan referensi. Sebaliknya, setelah gantiRekening(tabungan), objek yang ditunjuk tabungan tetap Budi dengan saldo 150000.0, meski method membuat objek baru atas nama Siti. Keadaan memori saat gantiRekening berjalan dapat digambarkan demikian:
frame main frame gantiRekening
tabungan ---+ rekening ---+
| |
v v
Rekening asli Rekening baru
(Budi, 150000.0) ("Siti", 999999 -> 0)Bedah perannya baris demi baris:
- Baris 4-6 (
isiSaldo): parameterrekeningberisi salinan alamat objek. Ekspresirekening.saldo = ...menelusuri alamat itu ke objek asli di heap dan menulis fieldsaldo— objek yang sama dibaca pemanggil, sehingga perubahan terlihat. - Baris 9 (
rekening = new Rekening("Siti", 999999);): assignment ini menimpa salinan alamat di frame method dengan alamat objek baru. Mulai baris ini, parameterrekeningdan variabeltabunganmenunjuk ke dua objek berbeda. - Baris 10 (
rekening.saldo = 0;): perubahan kini mengenai objek baru milik method — objek yang sama sekali tidak ditunjuk pemanggil, sehingga tidak berpengaruh padatabungan.
Kita kini memegang model mental yang kokoh: argument selalu disalin, tipe menentukan isi salinan. Model itu akan kembali terpakai ketika jumlah argument tidak lagi tetap — Java menyediakan parameter varargs untuk pemanggilan dengan nol sampai banyak sekaligus argument, materi Chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
MutasiObjek.java
Unduh berkaspublic class MutasiObjek {
// Mutasi field lewat salinan referensi: perubahan terlihat pemanggil
static void isiSaldo(Rekening rekening, double jumlah) {
rekening.saldo = rekening.saldo + jumlah;
}
// Mengganti referensi parameter ke objek baru: TIDAK terlihat pemanggil
static void gantiRekening(Rekening rekening) {
rekening = new Rekening("Siti", 999999);
rekening.saldo = 0;
}
public static void main(String[] args) {
Rekening tabungan = new Rekening("Budi", 100000);
System.out.println("awal : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
isiSaldo(tabungan, 50000);
System.out.println("setelah isi : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
gantiRekening(tabungan);
System.out.println("setelah ganti : " + tabungan.pemilik + " - Rp" + tabungan.saldo);
}
}
Rekening.java
Unduh berkaspublic class Rekening {
String pemilik;
double saldo;
Rekening(String pemilik, double saldo) {
this.pemilik = pemilik;
this.saldo = saldo;
}
}
TukarPrimitif.java
Unduh berkaspublic class TukarPrimitif {
static void tukar(int a, int b) {
int sementara = a;
a = b;
b = sementara;
System.out.println("di dalam tukar: a = " + a + ", b = " + b);
}
public static void main(String[] args) {
int x = 1;
int y = 9;
System.out.println("sebelum : x = " + x + ", y = " + y);
tukar(x, y);
System.out.println("sesudah : x = " + x + ", y = " + y);
}
}