Anggapan bahwa Java mengirim objek ke metode dengan pass by reference masih sering muncul, padahal spesifikasi bahasanya tidak pernah mengatakan demikian. Java menyalin nilai pada setiap pemanggilan metode, dan untuk objek yang disalin adalah referensinya, bukan objeknya. Topik ini membuktikan kedua hal itu lewat kasus swap klasik yang selalu gagal, lalu menelusuri apa yang benar-benar terjadi pada frame metode dan objek di memori. Di akhir, diperlihatkan pola yang benar bila sebuah metode memang harus mengubah data milik pemanggilnya.
Swap yang selalu gagal#
Kasus swap adalah uji yang paling telak. Dua variabel ditukar isinya lewat metode yang tampak benar, tetapi hasilnya tidak pernah berubah. Program berikut memuat metode swap yang menukar dua parameter int, lalu memanggilnya dari main dengan dua variabel yang jelas terlihat.
public class UjiSwap {
static void swap(int a, int b) {
int sementara = a;
a = b;
b = sementara;
}
public static void main(String[] args) {
int x = 3;
int y = 7;
System.out.println("Sebelum: x=" + x + ", y=" + y);
swap(x, y);
System.out.println("Sesudah: x=" + x + ", y=" + y);
}
}Sebelum: x=3, y=7
Sesudah: x=3, y=7Nilai x dan y tetap 3 dan 7 setelah swap selesai dijalankan. Pertukaran memang terjadi di dalam metode, hanya saja terjadi pada a dan b, yaitu salinan dari x dan y. Setelah metode berakhir, salinan itu musnah bersama frame-nya, sedangkan x dan y tidak pernah disentuh sedikit pun.
Yang disalin adalah nilai#
Setiap pemanggilan metode membuat frame baru di stack, lengkap dengan slot untuk parameter dan variabel lokal. Saat argumen dikirim, Java menyalin isinya ke slot parameter. Untuk tipe primitif, yang disalin adalah nilai angkanya sendiri, sehingga a dan b berdiri sepenuhnya terpisah dari x dan y. Mengubah a berarti mengubah satu sel di frame metode, bukan sel milik pemanggil.
// tidak-dijalankan
static void gandakan(int angka) {
angka = angka * 2; // hanya salinan di dalam metode yang berubah
}Perilaku ini berlaku seragam untuk seluruh tipe primitif, dari byte sampai double, termasuk boolean dan char. Tidak ada jalur khusus atau pengecualian untuk tipe tertentu. Karena itu, metode yang ingin melaporkan hasil pengolahan atas primitif mengembalikan nilainya lewat return, bukan mengandalkan perubahan pada parameternya.
Objek juga disalin referensinya#
Untuk tipe objek, nilai yang tersimpan di variabel bukan objeknya, melainkan referensi yang menunjuk ke objek di heap. Nilai inilah yang disalin ke parameter. Salinan referensi menunjuk ke objek yang sama dengan referensi pemanggil, sehingga mengubah isi objek lewat parameter terlihat oleh pemanggil. Sebaliknya, mengganti parameter agar menunjuk objek lain hanya mengubah salinannya, dan pemanggil tetap memegang referensi lama.
public class UjiReferensi {
static class Keranjang {
int isi;
}
static void isiUlang(Keranjang k) {
k.isi = 99; // mutasi lewat salinan referensi, terlihat pemanggil
k = new Keranjang(); // mengganti salinan referensi, tidak berdampak
k.isi = 7;
}
public static void main(String[] args) {
Keranjang keranjang = new Keranjang();
keranjang.isi = 10;
System.out.println("Sebelum: " + keranjang.isi);
isiUlang(keranjang);
System.out.println("Sesudah: " + keranjang.isi);
}
}Sebelum: 10
Sesudah: 99Metode isiUlang memuat dua baris yang dampaknya bertolak belakang. Baris k.isi = 99 mengubah isi objek lewat salinan referensi, dan perubahan itu terbaca oleh main. Baris k = new Keranjang() mengganti salinan referensi agar menunjuk objek baru, sehingga k.isi = 7 pada baris berikutnya menyasar objek yang tidak dikenal main. Dua perilaku itu digambarkan berdampingan sebagai berikut.
Pada panel pertama, keranjang dan k menunjuk objek yang sama, sehingga mutasi lewat k sampai ke pemanggil. Pada panel kedua, k sudah menunjuk objek baru, sementara keranjang tetap memegang objek lama yang isinya berubah menjadi 99. Kedua panel itu tetap konsisten dengan satu aturan yang sama, yaitu penyalinan nilai.
Rancangan yang menghasilkan efek#
Bila sebuah metode memang harus mengubah data pemanggil, jalan keluarnya ada dua. Pertama, bungkus nilainya dalam objek lalu ubah isi objek tersebut lewat referensinya, seperti pada metode tukar berikut. Kedua, kembalikan hasilnya sebagai nilai kembalian dan biarkan pemanggil menampungnya.
public class UjiPasangan {
static class Pasangan {
int pertama;
int kedua;
}
static void tukar(Pasangan p) {
int sementara = p.pertama;
p.pertama = p.kedua;
p.kedua = sementara;
}
public static void main(String[] args) {
Pasangan p = new Pasangan();
p.pertama = 3;
p.kedua = 7;
System.out.println("Sebelum: " + p.pertama + " dan " + p.kedua);
tukar(p);
System.out.println("Sesudah: " + p.pertama + " dan " + p.kedua);
}
}Sebelum: 3 dan 7
Sesudah: 7 dan 3Kali ini pertukaran berhasil karena yang diubah adalah field milik objek, bukan parameter yang menampung primitif. Pola serupa berlaku untuk array, karena array juga objek. Isi elemennya bisa diubah lewat metode, sedangkan mengganti referensi array di dalam metode tidak berdampak apa pun pada pemanggil. String sifatnya berbeda lagi karena isinya tidak bisa diubah sama sekali, sehingga metode yang memproses teks selalu mengembalikan String baru untuk ditampung pemanggil.
Kesalahan umum#
- Mengharapkan swap primitif bekerja lalu menganggap kompilatornya salah. Parameter primitif memang tidak bisa mengubah variabel pemanggil. Kembalikan hasil lewat
returnatau gunakan objek pembungkus sepertiUjiPasangan. - Mengira
k = new Keranjang()di dalam metode mengganti objek milik pemanggil. Yang berganti hanya salinan referensinya. Kembalikan referensi baru sebagai nilai kembalian bila pemanggil membutuhkan objek baru. - Mencari cara mengubah isi
Stringlewat metode. IsiStringtidak dapat diubah, sehingga hasil pengolahannya selaluStringbaru yang perlu ditampung. - Mengira field objek ikut berganti saat parameter diganti objek baru. Field tetap melekat pada objek lama yang dirujuk pemanggil.
Ringkasan#
- Java selalu memakai pass by value pada setiap pemanggilan metode, tanpa pengecualian.
- Untuk primitif yang disalin adalah nilainya, sedangkan untuk objek yang disalin adalah referensinya, bukan objeknya.
- Mutasi isi objek lewat salinan referensi terlihat oleh pemanggil, sedangkan penggantian referensi tidak berdampak.
- Efek yang diinginkan dicapai dengan mengubah isi objek atau mengembalikan nilai baru.
Referensi#
- Passing Information to a Method or a Constructor — Oracle — menyatakan bahwa parameter primitif maupun objek dikirim ke metode secara by value.
- Frames — JVM Specification, Java SE 21 Edition — struktur frame di stack tempat parameter dan variabel lokal hidup.