Sepanjang Section ini kita menulis fungsi yang semakin terpisah-pisah menjadi potongan kecil. Sekarang tiba pertanyaan yang menentukan kualitasnya: apakah setiap potongan itu murni? Sebuah fungsi disebut pure function bila hasilnya dihitung semata-mata dari argumennya, tanpa membaca dan tanpa mengubah apa pun di luar dirinya. Konsekuensinya sederhana namun kuat: memanggil fungsi murni dua kali dengan argumen yang sama selalu memberi hasil yang sama, dan dunia luar tidak berubah sedikit pun. Pada Chapter ini kita membedah kontras murni vs efek samping dengan bukti eksekusi, lalu berbicara jujur tentang batas praktisnya di Java — karena program yang benar-benar murni tidak akan pernah bisa mencetak apa pun.

Definisi Operasional Murni dan Tidak Murni #

Tiga pertanyaan cukup untuk menguji kemurnian sebuah method. Pertama, apakah hasilnya hanya fungsi dari parameter? Kedua, apakah method membaca sesuatu di luar parameter — variabel static, isu waktu, isi berkas? Ketiga, apakah method mengubah sesuatu di luar dirinya — field objek lain, isi collection milik pemanggil, layar terminal? Jawaban “ya” pada pertanyaan kedua atau ketiga berarti method itu punya side effect (efek samping) atau membaca keadaan luar — dua-duanya menurunkan kemurnian. Program berikut menjalankan tugas yang sama dua cara: satu murni, satu mengubah data pemanggil:

import java.util.ArrayList;
import java.util.List;

public class MurniVsEfekSamping {
    // PURE: hasil hanya dihitung dari masukan, dunia luar tidak tersentuh
    static List<Integer> ambilGenap(List<Integer> sumber) {
        List<Integer> hasil = new ArrayList<>();
        for (int n : sumber) {
            if (n % 2 == 0) {
                hasil.add(n);
            }
        }
        return hasil;
    }

    // IMPURE: mengubah isi list milik pemanggil (side effect pada objek luar)
    static void hapusGanjil(List<Integer> target) {
        target.removeIf(n -> n % 2 != 0);
    }

    public static void main(String[] args) {
        List<Integer> data = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

        List<Integer> genap = ambilGenap(data);
        System.out.println("Hasil fungsi murni  : " + genap);
        System.out.println("data setelah murni  : " + data);

        hapusGanjil(data);
        System.out.println("data setelah impure : " + data);
    }
}

Output yang Dihasilkan:

text
Hasil fungsi murni  : [2, 4, 6]
data setelah murni  : [1, 2, 3, 4, 5, 6]
data setelah impure : [2, 4, 6]

Bandingkan kedua method pada berkas itu:

  1. ambilGenap (murni): method ini membangun ArrayList baru, mengisi hasilnya, dan mengembalikan list itu. Method membaca parameter sumber tanpa mengubah satu elemen pun — terbukti pada keluaran baris kedua: data masih utuh [1, 2, 3, 4, 5, 6] setelah pemanggilan.
  2. hapusGanjil (tidak murni): method ini tidak mengembalikan apa pun; justru isinya bekerja pada target — list milik pemanggil — lewat removeIf. Setelah pemanggilan, data berubah menjadi [2, 4, 6]. Efeknya nyata dan menetap pada objek luar.
  3. Perhatikan tanda tangan sebagai petunjuk: List<Integer> ambilGenap(List) yang mengembalikan nilai tanpa void, versus void hapusGanjil(List) yang mengubah argumennya. Tanda tangan bukan bukti hukum — method pengembalian nilai pun bisa curang — tetapi pola “kembalikan hasil baru” versus “ubah yang lama” adalah indikator visual yang berguna saat membaca kode.

Mengapa Fungsi Murni Mudah Diuji #

Kemurnian adalah hadiah langsung untuk pengujian. Untuk menguji fungsi murni, kita cukup: panggil, bandingkan dengan nilai harapan, selesai — tanpa menyiapkan keadaan dunia dan tanpa memeriksa keadaan dunia sesudahnya. Fungsi ber-efek samping memaksa penguji memikirkan hal-hal di luar nilai kembalian:

java UjiMurni.java
public class UjiMurni {
    // Murni: hasil hanya dari dua argumen
    static int totalDiskon(int harga, int persen) {
        return harga * persen / 100;
    }

    // Keadaan luar yang dibaca dan diubah fungsi berikut
    static int diskonTercatat = 0;

    // Tidak murni: mengembalikan nilai DAN mengubah keadaan luar
    static int diskonDenganLog(int harga, int persen) {
        int hasil = harga * persen / 100;
        diskonTercatat += hasil;
        return hasil;
    }

    public static void main(String[] args) {
        // Menguji fungsi murni: panggil, bandingkan, selesai
        System.out.println("totalDiskon(100000, 10) == 10000 : "
                + (totalDiskon(100000, 10) == 10000));

        // Menguji fungsi ber-efek: nilai kembalian sama, keadaan luar berubah
        System.out.println("diskonDenganLog(100000, 10)      : "
                + diskonDenganLog(100000, 10));
        System.out.println("keadaan diskonTercatat           : " + diskonTercatat);
        System.out.println("diskonDenganLog(50000, 10)       : "
                + diskonDenganLog(50000, 10));
        System.out.println("keadaan setelahnya               : " + diskonTercatat);
    }
}

Output yang Dihasilkan:

text
totalDiskon(100000, 10) == 10000 : true
diskonDenganLog(100000, 10)      : 10000
keadaan diskonTercatat           : 10000
diskonDenganLog(50000, 10)       : 5000
keadaan setelahnya               : 15000

Bedah pengujian kedua fungsi tersebut:

  1. Baris 14-15 (totalDiskon(100000, 10) == 10000): satu pemanggilan, satu perbandingan, selesai. Tidak ada keadaan yang perlu disiapkan karena fungsi tidak tahu dunia luar; true pada keluaran membuktikan kebenarannya.
  2. Baris 20-24 (diskonDenganLog(...)): nilai kembaliannya benar juga, tetapi keluaran baris keadaan diskonTercatat : 10000 memperlihatkan setengah pekerjaan yang belum diuji. Pemanggilan kedua menaikkan keadaan luar menjadi 15000 — artinya pengujian yang benar harus memeriksa riwayat pemanggilan, bukan hanya satu hasil.
  3. Urutan kini berperan: pada fungsi murni, urutan pemanggilan bebas diatur karena tidak ada yang saling menyerempet. Pada fungsi ber-efek, membalik urutan dua pemanggilan mengubah isi diskonTercatat di setiap titik — kelas masalah yang dihilangkan begitu kemurnian ditegakkan.

Batas Praktis di Java #

Program yang murni sepenuhnya tidak berguna sebagai program: System.out.println, penulisan berkas, permintaan jaringan, dan pembacaan waktu adalah side effect — dan justru itu tujuan program kita ada. Bahasa fungsional murni seperti Haskell memecahkan masalah ini dengan sistem tipe yang menandai efek; Java tidak punya penanda semacam itu, sehingga kemurnian menjadi disiplin arsitektur, bukan jaminan compiler. Prinsip yang biasa dipegang komunitas: functional core, imperative shell — seluruh aturan bisnis (hitung diskon, validasi, transformasi) ditulis sebagai fungsi murni di lapisan dalam, sedangkan I/O, logging, dan persistensi dipusatkan di lapisan luar yang tipis. Dengan bentuk itu, mayoritas kode tetap semudah totalDiskon untuk diuji, sementara bagian yang menyerahkan dirinya kepada dunia luar tetap terkotak dan mudah ditemukan.

Dua pertahanan terakhir di tangan kita adalah aturan effectively final dari Chapter sebelumnya — yang mencegah lambda diam-diam menugaskan ulang variabel luar — dan kebiasaan mengembalikan koleksi baru alih-alih mengubah isi parameter. Kedua kebiasaan itu, plus pola desain yang akan kita rangkum pada Chapter penutup Section ini, membentuk gaya fungsional Java yang utuh dan siap dipakai di proyek sungguhan.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

MurniVsEfekSamping.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class MurniVsEfekSamping {
    // PURE: hasil hanya dihitung dari masukan, dunia luar tidak tersentuh
    static List<Integer> ambilGenap(List<Integer> sumber) {
        List<Integer> hasil = new ArrayList<>();
        for (int n : sumber) {
            if (n % 2 == 0) {
                hasil.add(n);
            }
        }
        return hasil;
    }

    // IMPURE: mengubah isi list milik pemanggil (side effect pada objek luar)
    static void hapusGanjil(List<Integer> target) {
        target.removeIf(n -> n % 2 != 0);
    }

    public static void main(String[] args) {
        List<Integer> data = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

        List<Integer> genap = ambilGenap(data);
        System.out.println("Hasil fungsi murni  : " + genap);
        System.out.println("data setelah murni  : " + data);

        hapusGanjil(data);
        System.out.println("data setelah impure : " + data);
    }
}

UjiMurni.java

Unduh berkas
java
public class UjiMurni {
    // Murni: hasil hanya dari dua argumen
    static int totalDiskon(int harga, int persen) {
        return harga * persen / 100;
    }

    // Keadaan luar yang dibaca dan diubah fungsi berikut
    static int diskonTercatat = 0;

    // Tidak murni: mengembalikan nilai DAN mengubah keadaan luar
    static int diskonDenganLog(int harga, int persen) {
        int hasil = harga * persen / 100;
        diskonTercatat += hasil;
        return hasil;
    }

    public static void main(String[] args) {
        // Menguji fungsi murni: panggil, bandingkan, selesai
        System.out.println("totalDiskon(100000, 10) == 10000 : "
                + (totalDiskon(100000, 10) == 10000));

        // Menguji fungsi ber-efek: nilai kembalian sama, keadaan luar berubah
        System.out.println("diskonDenganLog(100000, 10)      : "
                + diskonDenganLog(100000, 10));
        System.out.println("keadaan diskonTercatat           : " + diskonTercatat);
        System.out.println("diskonDenganLog(50000, 10)       : "
                + diskonDenganLog(50000, 10));
        System.out.println("keadaan setelahnya               : " + diskonTercatat);
    }
}