Coba tebak hasil 0.1 + 0.2 == 0.3 di Java: jawabannya false. Angka desimal yang tampak sederhana tersebut menghasilkan 0.30000000000000004 — dan bukan karena bug, melainkan karena konsekuensi matematika dari standar IEEE 754 yang dipakai float dan double. Komputer menyimpan bilangan dalam biner, dan kebanyakan angka desimal tidak punya representasi biner yang persis. Pada Chapter ini kita bedah mengapa ketelitian floating-point selalu terbatas, bagaimana membandingkan angka desimal dengan benar, dan kapan harus beralih ke BigDecimal — khususnya untuk nilai uang.

Representasi Biner IEEE 754 #

Tipe double menyimpan angka dalam 64 bit: 1 bit tanda, 11 bit eksponen, dan 52 bit significand (mantis). Skema ini mirip notasi ilmiah biner: nilai = ±1.xxx…×2^eksponen. Dengan 52 bit significand, double menampung sekitar 15-16 digit desimal signifikan — sangat banyak, tetapi terbatas.

Persoalan muncul karena pecahan desimal seperti 0.1 sama sekali tidak berbentuk pecahan biner yang berakhir. Di dunia basis dua, 0.1 adalah barisan tak berujung — persis seperti 1/3 = 0.3333… di dunia basis sepuluh. Konsekuensinya:

  • Literal 0.1 disimpan sebagai nilai biner terdekat, sedikit di atas atau di bawah 0.1 yang sebenarnya.
  • Penjumlahan 0.1 + 0.2 menjumlah dua nilai pendekatan tersebut, menghasilkan 0.30000000000000004.
  • Literal 0.3 sendiri adalah pendekatan lain yang bit-nya berbeda — sehingga perbandingan == menghasilkan false.

Anomali Presisi dalam Praktik #

Program berikut memperlihatkan dua anomali paling klasik: penjumlahan langsung dan akumulasi berulang:

public class PresisiDasar {
    public static void main(String[] args) {
        double hasil = 0.1 + 0.2;
        System.out.println("0.1 + 0.2 = " + hasil);
        System.out.println("hasil == 0.3 : " + (hasil == 0.3));

        double akumulasi = 0.0;
        for (int i = 0; i < 10; i++) {
            akumulasi += 0.1;
        }
        System.out.println("0.1 sepuluh kali = " + akumulasi);
        System.out.println("akumulasi == 1.0 : " + (akumulasi == 1.0));
    }
}

Output yang Dihasilkan:

text
0.1 + 0.2 = 0.30000000000000004
hasil == 0.3 : false
0.1 sepuluh kali = 0.9999999999999999
akumulasi == 1.0 : false

Dua pengamatan dari keluaran di atas:

  1. Baris 4-6 (0.1 + 0.2): hasilnya 0.30000000000000004 — digit ke-17 muncul dari sisa pembulatan biner. Perbandingan == dengan 0.3 menghasilkan false karena kedua nilai berpola bit berbeda.
  2. Baris 8-14 (akumulasi loop): menjumlahkan 0.1 sebanyak sepuluh kali menghasilkan 0.9999999999999999, bukan 1.0. Kesalahan kecil tiap langkah terakumulasi; makin panjang rantai perhitungan, makin jauh hasilnya bisa bergeser. Inilah alasan program ilmiah dan finansial menjauhi penjumlahan berulang pada double.

Perbandingan dengan Toleransi #

Cara yang benar membandingkan floating-point adalah toleransi kesalahan (epsilon): dua angka dianggap sama bila selisihnya lebih kecil dari ambang yang kita tetapkan. Untuk perhitungan umum, nilai 1e-9 (satu per miliar) kerap memadai:

public class BandingToleransi {
    public static void main(String[] args) {
        double hasil = 0.1 + 0.2;
        double target = 0.3;
        double eps = 1e-9;   // toleransi 0.000000001

        System.out.println("selisih       : " + (hasil - target));
        System.out.println("banding ==    : " + (hasil == target));
        System.out.println("banding eps   : " + (Math.abs(hasil - target) < eps));

        double akumulasi = 0.0;
        for (int i = 0; i < 10; i++) {
            akumulasi += 0.1;
        }
        System.out.println("akumulasi     : " + akumulasi);
        System.out.println("hampir 1.0?   : " + (Math.abs(akumulasi - 1.0) < eps));
    }
}

Output yang Dihasilkan:

text
selisih       : 5.551115123125783E-17
banding ==    : false
banding eps   : true
akumulasi     : 0.9999999999999999
hampir 1.0?   : true

Mari kita bedah hasilnya:

  1. Baris 10-11 (selisih): jarak 0.1 + 0.2 ke 0.3 hanyalah 5.551115123125783E-17 — selisih triliunan kali lebih kecil daripada toleransi 1e-9. Secara matematis keduanya “sama”, secara bit tidak.
  2. Baris 12-13: perbandingan == tetap false, sedangkan Math.abs(hasil - target) < eps bernilai true — inilah cara yang harus dipakai.
  3. Baris 15-21 (akumulasi): 0.9999999999999999 lolos uji “hampir 1.0” dengan selisih sekitar 1E-16, jauh di bawah ambang. Pemilihan eps sendiri bergantung skala angka: untuk nilai sekitar jutaan, toleransi relatif seperti eps * Math.max(Math.abs(a), Math.abs(b)) lebih tepat daripada toleransi mutlak.

Pustaka standar juga menyediakan Double.compare(a, b) untuk pengurutan (mengembalikan -1/0/1) — tetapi untuk uji kesamaan hasil hitungan, toleransi tetap jawabannya.

BigDecimal untuk Angka Uang #

Untuk nilai yang tidak boleh kehilangan satu digit pun — uang, tarif, kurs — solusi definitifnya adalah java.math.BigDecimal. Kelas ini menyimpan angka desimal sebagai digit dan skala (bukan biner), sehingga 0.1 tersimpan persis sebagai 0,1. Satu aturan emas: bangun dari String, bukan dari double — new BigDecimal("0.1") tepat, sedangkan new BigDecimal(0.1) justru membekukan cacat biner 0.100000000000000005551115123125782… ke dalam objek.

Program berikut membandingkan jalur double dan BigDecimal untuk kasus bunga tahunan:

java UangPresisi.java
import java.math.BigDecimal;

public class UangPresisi {
    public static void main(String[] args) {
        double hargaDouble = 0.1 + 0.2;
        System.out.println("double    : " + hargaDouble);

        BigDecimal harga = new BigDecimal("0.1").add(new BigDecimal("0.2"));
        System.out.println("BigDecimal: " + harga);

        BigDecimal saldo = new BigDecimal("1000000.00");
        BigDecimal bunga = new BigDecimal("0.11");
        BigDecimal bungaTahunan = saldo.multiply(bunga);
        System.out.println("bunga     : " + bungaTahunan);
        System.out.println("total     : " + saldo.add(bungaTahunan));
    }
}

Output yang Dihasilkan:

text
double    : 0.30000000000000004
BigDecimal: 0.3
bunga     : 110000.0000
total     : 1110000.0000

Dua catatan penting dari program di atas:

  1. Baris 5-8: jalur double mencetak 0.30000000000000004, sedangkan penjumlahan dua BigDecimal menghasilkan 0.3 — persis seperti yang diharapkan mata manusia dan kalkulator.
  2. Baris 10-14: perhitungan keuangan 1000000.00 × 0.11 = 110000.0000 tercetak utuh tanpa pembulatan sesat. BigDecimal tidak punya operator aritmetika; semua operasi berbentuk method (add, multiply, subtract, divide) yang mengembalikan objek baru — BigDecimal bersifat immutable. Untuk pembagian yang tidak habis, gunakan divide(x, skala, RoundingMode) agar tidak melempar exception.

Presisi floating-point menutup pembahasan keterbatasan tipe primitive. Sekarang kita beralih ke jembatan antara dunia primitive dan dunia objek: tipe int tidak punya method, tidak bisa masuk ke List, tidak bisa bernilai null — sampai class pembungkusnya hadir. Chapter berikutnya memperkenalkan wrapper classes Integer, Double, dan kawan-kawannya.

Referensi #

Berkas Sumber

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

BandingToleransi.java

Unduh berkas
java
public class BandingToleransi {
    public static void main(String[] args) {
        double hasil = 0.1 + 0.2;
        double target = 0.3;
        double eps = 1e-9;   // toleransi 0.000000001

        System.out.println("selisih       : " + (hasil - target));
        System.out.println("banding ==    : " + (hasil == target));
        System.out.println("banding eps   : " + (Math.abs(hasil - target) < eps));

        double akumulasi = 0.0;
        for (int i = 0; i < 10; i++) {
            akumulasi += 0.1;
        }
        System.out.println("akumulasi     : " + akumulasi);
        System.out.println("hampir 1.0?   : " + (Math.abs(akumulasi - 1.0) < eps));
    }
}

PresisiDasar.java

Unduh berkas
java
public class PresisiDasar {
    public static void main(String[] args) {
        double hasil = 0.1 + 0.2;
        System.out.println("0.1 + 0.2 = " + hasil);
        System.out.println("hasil == 0.3 : " + (hasil == 0.3));

        double akumulasi = 0.0;
        for (int i = 0; i < 10; i++) {
            akumulasi += 0.1;
        }
        System.out.println("0.1 sepuluh kali = " + akumulasi);
        System.out.println("akumulasi == 1.0 : " + (akumulasi == 1.0));
    }
}

UangPresisi.java

Unduh berkas
java
import java.math.BigDecimal;

public class UangPresisi {
    public static void main(String[] args) {
        double hargaDouble = 0.1 + 0.2;
        System.out.println("double    : " + hargaDouble);

        BigDecimal harga = new BigDecimal("0.1").add(new BigDecimal("0.2"));
        System.out.println("BigDecimal: " + harga);

        BigDecimal saldo = new BigDecimal("1000000.00");
        BigDecimal bunga = new BigDecimal("0.11");
        BigDecimal bungaTahunan = saldo.multiply(bunga);
        System.out.println("bunga     : " + bungaTahunan);
        System.out.println("total     : " + saldo.add(bungaTahunan));
    }
}