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:

text
sebelum        : x = 1, y = 9
di dalam tukar: a = 9, b = 1
sesudah        : x = 1, y = 9

Hasil 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:

  1. Baris 19 (tukar(x, y);): nilai 1 dan 9 disalin ke parameter a dan b di stack frame method. Variabel x dan y tetap di frame main dengan nilainya masing-masing.
  2. Baris 3-6 (int sementara = a; a = b; b = sementara;): tiga pernyataan ini menukar isi parameter a dan b — dua salinan di frame method. Rangkaian penukaran tidak pernah menyentuh variabel x dan y.
  3. Baris 20 (System.out.println("sesudah ...")): frame tukar sudah dibongkar, parameter a dan b ikut terbuang bersama frame-nya. Variabel x dan y tidak 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:

java Rekening.java
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:

java MutasiObjek.java
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:

text
awal          : Budi - Rp100000.0
setelah isi   : Budi - Rp150000.0
setelah ganti : Budi - Rp150000.0

Dua 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:

text tabungan dan rekening saat gantiRekening berjalan
frame main          frame gantiRekening
tabungan ---+       rekening ---+
            |                   |
            v                   v
       Rekening asli       Rekening baru
       (Budi, 150000.0)    ("Siti", 999999 -> 0)

Bedah perannya baris demi baris:

  1. Baris 4-6 (isiSaldo): parameter rekening berisi salinan alamat objek. Ekspresi rekening.saldo = ... menelusuri alamat itu ke objek asli di heap dan menulis field saldo — objek yang sama dibaca pemanggil, sehingga perubahan terlihat.
  2. Baris 9 (rekening = new Rekening("Siti", 999999);): assignment ini menimpa salinan alamat di frame method dengan alamat objek baru. Mulai baris ini, parameter rekening dan variabel tabungan menunjuk ke dua objek berbeda.
  3. Baris 10 (rekening.saldo = 0;): perubahan kini mengenai objek baru milik method — objek yang sama sekali tidak ditunjuk pemanggil, sehingga tidak berpengaruh pada tabungan.

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 berkas
java
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);
    }
}

Rekening.java

Unduh berkas
java
public class Rekening {

    String pemilik;
    double saldo;

    Rekening(String pemilik, double saldo) {
        this.pemilik = pemilik;
        this.saldo = saldo;
    }
}

TukarPrimitif.java

Unduh berkas
java
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);
    }
}