Pass by Value, Selalu

Membuktikan bahwa Java menyalin nilai, termasuk menyalin referensi objek, lewat kasus swap klasik.

Daftar isi

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.

java UjiSwap.java
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);
    }
}
text Keluaran
Sebelum: x=3, y=7
Sesudah: x=3, y=7

Nilai 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.

java Contoh: parameter adalah variabel yang terpisah
// 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.

java UjiReferensi.java
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);
    }
}
text Keluaran
Sebelum: 10
Sesudah: 99

Metode 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.

1. Saat metode dipanggilframe mainkeranjangframe isiUlangkheapobjek Keranjangisi = 102. Setelah k = new Keranjang()frame mainkeranjangframe isiUlangkheapobjek Keranjangisi = 99objek Keranjangisi = 7
Yang disalin ke parameter adalah referensi, bukan objek

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.

java UjiPasangan.java
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);
    }
}
text Keluaran
Sebelum: 3 dan 7
Sesudah: 7 dan 3

Kali 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 return atau gunakan objek pembungkus seperti UjiPasangan.
  • 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 String lewat metode. Isi String tidak dapat diubah, sehingga hasil pengolahannya selalu String baru 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#