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.1disimpan sebagai nilai biner terdekat, sedikit di atas atau di bawah0.1yang sebenarnya. - Penjumlahan
0.1 + 0.2menjumlah dua nilai pendekatan tersebut, menghasilkan0.30000000000000004. - Literal
0.3sendiri adalah pendekatan lain yang bit-nya berbeda — sehingga perbandingan==menghasilkanfalse.
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:
0.1 + 0.2 = 0.30000000000000004
hasil == 0.3 : false
0.1 sepuluh kali = 0.9999999999999999
akumulasi == 1.0 : falseDua pengamatan dari keluaran di atas:
- Baris 4-6 (
0.1 + 0.2): hasilnya0.30000000000000004— digit ke-17 muncul dari sisa pembulatan biner. Perbandingan==dengan0.3menghasilkanfalsekarena kedua nilai berpola bit berbeda. - Baris 8-14 (akumulasi loop): menjumlahkan
0.1sebanyak sepuluh kali menghasilkan0.9999999999999999, bukan1.0. Kesalahan kecil tiap langkah terakumulasi; makin panjang rantai perhitungan, makin jauh hasilnya bisa bergeser. Inilah alasan program ilmiah dan finansial menjauhi penjumlahan berulang padadouble.
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:
selisih : 5.551115123125783E-17
banding == : false
banding eps : true
akumulasi : 0.9999999999999999
hampir 1.0? : trueMari kita bedah hasilnya:
- Baris 10-11 (
selisih): jarak0.1 + 0.2ke0.3hanyalah5.551115123125783E-17— selisih triliunan kali lebih kecil daripada toleransi1e-9. Secara matematis keduanya “sama”, secara bit tidak. - Baris 12-13: perbandingan
==tetapfalse, sedangkanMath.abs(hasil - target) < epsbernilaitrue— inilah cara yang harus dipakai. - Baris 15-21 (akumulasi):
0.9999999999999999lolos uji “hampir 1.0” dengan selisih sekitar1E-16, jauh di bawah ambang. Pemilihanepssendiri bergantung skala angka: untuk nilai sekitar jutaan, toleransi relatif sepertieps * 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:
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:
double : 0.30000000000000004
BigDecimal: 0.3
bunga : 110000.0000
total : 1110000.0000Dua catatan penting dari program di atas:
- Baris 5-8: jalur
doublemencetak0.30000000000000004, sedangkan penjumlahan duaBigDecimalmenghasilkan0.3— persis seperti yang diharapkan mata manusia dan kalkulator. - Baris 10-14: perhitungan keuangan
1000000.00 × 0.11 = 110000.0000tercetak utuh tanpa pembulatan sesat.BigDecimaltidak punya operator aritmetika; semua operasi berbentuk method (add,multiply,subtract,divide) yang mengembalikan objek baru —BigDecimalbersifat immutable. Untuk pembagian yang tidak habis, gunakandivide(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 #
- Java API: BigDecimal
- JLS SE25 §4.2.3: Floating-Point Types, Formats, and Values
- Oracle: Numbers in Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BandingToleransi.java
Unduh berkaspublic 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 berkaspublic 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 berkasimport 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));
}
}