Sai số dấu phẩy động
Vấn đề
Phần tiêu đề “Vấn đề”float và double lưu trữ số thực theo chuẩn dấu phẩy động IEEE 754 - không thể biểu diễn chính xác tuyệt đối hầu hết số thập phân, dẫn đến những kết quả trông “sai” một cách khó hiểu:
System.out.println(0.1 + 0.2); // 0.30000000000000004 - KHÔNG phải 0.3!
double price = 0.1;double total = price + price + price;System.out.println(total == 0.3); // false - dù "trông có vẻ" phải bằng nhau!Vì sao xảy ra?
Phần tiêu đề “Vì sao xảy ra?”Máy tính lưu trữ số thực dưới dạng nhị phân. Một số phân số thập phân đơn giản như 0.1 không có biểu diễn nhị phân hữu hạn chính xác (giống như 1/3 không viết hết được dưới dạng thập phân) - Java phải làm tròn đến giá trị gần nhất có thể biểu diễn được, gây ra sai số rất nhỏ tích lũy qua các phép tính.
Vì sao nguy hiểm?
Phần tiêu đề “Vì sao nguy hiểm?”Sai số này đặc biệt nguy hiểm khi:
- So sánh bằng nhau (
==) giữa hai số thực - gần như không bao giờ nên làm điều này - Tính toán liên quan đến tiền tệ - sai số nhỏ tích lũy qua nhiều giao dịch có thể gây chênh lệch đáng kể
double balance = 0;for (int i = 0; i < 10; i++) { balance += 0.1;}System.out.println(balance); // 0.9999999999999999 - KHÔNG chính xác là 1.0!Giải pháp
Phần tiêu đề “Giải pháp”1. Không bao giờ so sánh == giữa hai số thực - so sánh với một sai số cho phép (epsilon):
double a = 0.1 + 0.2;double b = 0.3;double epsilon = 1e-9;
System.out.println(Math.abs(a - b) < epsilon); // true - so sánh "gần bằng nhau"2. Dùng BigDecimal cho các tính toán cần độ chính xác cao, đặc biệt là tiền tệ:
import java.math.BigDecimal;
BigDecimal price1 = new BigDecimal("0.1");BigDecimal price2 = new BigDecimal("0.2");BigDecimal total = price1.add(price2);
System.out.println(total); // 0.3 - CHÍNH XÁC⚠️ Khi tạo BigDecimal, luôn dùng constructor nhận chuỗi (new BigDecimal("0.1")), không dùng double (new BigDecimal(0.1)) - vì 0.1 dưới dạng double đã bị làm tròn từ trước, sai số sẽ “kế thừa” vào BigDecimal.
Tóm tắt
Phần tiêu đề “Tóm tắt”
float/doublekhông thể biểu diễn chính xác hầu hết số thập phân - phép tính có thể cho kết quả sai số rất nhỏ.
- Không bao giờ so sánh
==giữa hai số thực - dùng sai số cho phép (epsilon)- Dùng
BigDecimal(khởi tạo từ chuỗi) cho tính toán cần độ chính xác cao, đặc biệt là tiền tệ