1. Giới thiệu
Có vẻ như nếu máy tính "thông minh", thì 0.1 + 0.2 phải đơn giản là 0.3. Nhưng không hẳn vậy. Hãy cùng tìm hiểu qua một ví dụ đơn giản.
double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // Chương trình sẽ in ra gì?
Giờ hãy thử so sánh với 0.3:
System.out.println(sum == 0.3); // Ở đây sẽ là true hay false?
Nếu bạn thấy false, đừng ngạc nhiên!
Nguyên nhân nằm ở biểu diễn số trong bộ nhớ
Máy tính làm việc với số theo hệ nhị phân. Nhưng không phải mọi phân số thập phân đều có thể biểu diễn bằng phân số nhị phân hữu hạn, giống như 1/3 không thể ghi chính xác bằng thập phân (0.333...). Chẳng hạn, 0.1 trong hệ nhị phân là một phân số vô hạn, nên phải "làm tròn" để lưu trữ.
Nói một cách đời thường, double đôi khi "trông như" đang lưu chính xác số của bạn, nhưng thực ra chỉ lưu một giá trị xấp xỉ rất gần.
2. Những "trục trặc" nào thường gặp khi tính toán với double?
Hãy xem một vài ví dụ thực tế.
Ví dụ 1. "Phép màu" kinh điển 0.1 + 0.2
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum); // 0.30000000000000004
System.out.println(sum == 0.3); // false
Máy tính in ra không phải 0.3 mà là 0.30000000000000004. Chênh lệch rất nhỏ, nhưng nếu bạn làm, chẳng hạn, tài chính — điều này đã là nghiêm trọng.
Ví dụ 2. Cộng lặp
double result = 0;
for (int i = 0; i < 10; i++)
{
result += 0.1;
}
System.out.println(result); // 0.9999999999999999
Mong muốn 1.0 — nhưng nhận được nhỏ hơn một chút. Lý do lại là việc làm tròn bên trong double.
Vì sao điều này quan trọng trong các bài toán thực tế
Nhiều người nghĩ: "Sai chút xíu thì có sao!" Hãy xem một ví dụ trong lĩnh vực thanh toán.
Giả sử ngân hàng trực tuyến của bạn cộng 100 giao dịch, mỗi giao dịch 0.1 euro. Nếu chương trình của bạn "mất" mỗi vòng một phần một trăm nghìn euro, ở quy mô ngân hàng bạn sẽ "thất thoát" tiền thật. Khi đó kế toán sẽ đến hỏi bạn: "Tiền của chúng tôi đâu?!"
3. So sánh số thực đúng cách
Vì double thường không thể lưu đúng giá trị bạn kỳ vọng, so sánh trực tiếp bằng == có thể phản tác dụng. Thay vào đó, thông lệ là so sánh giá trị tuyệt đối của hiệu với một số rất nhỏ (epsilon).
Ví dụ so sánh với ngưỡng sai số
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;
if (Math.abs(a - b) < epsilon)
{
System.out.println("Gần như bằng nhau!"); // So sánh kiểu này an toàn hơn
}
Ở đây ta nói: "Nếu chênh lệch giữa hai số nhỏ hơn một phần triệu, coi như chúng bằng nhau".
Lưu ý: hàm Math.abs(value) trả về giá trị tuyệt đối (mô-đun) của số truyền vào.
4. Các giá trị đặc biệt của double: Infinity, NaN, -Infinity
Kiểu double không chỉ lưu số mà còn có các giá trị đặc biệt. Chúng xuất hiện trong những tình huống mà các thầy dạy toán thường nghiêm cấm sinh viên làm.
Vô cực (Infinity)
Điều gì xảy ra nếu chia 1 cho 0?
double result = 1.0 / 0.0;
System.out.println(result); // Infinity
Trong Java (và nhiều ngôn ngữ), chia cho 0 với double không ném ngoại lệ! Thay vào đó, kết quả trở thành giá trị đặc biệt "vô cực dương".
Âm vô cực (-Infinity)
Nếu chia một số âm cho 0, bạn sẽ nhận âm vô cực:
double result = -1.0 / 0.0;
System.out.println(result); // -Infinity
"Không phải số" (NaN — Not a Number)
Nếu làm điều gì đó "kỳ cục", ví dụ thử tính căn bậc hai của một số âm:
double result = Math.sqrt(-1);
System.out.println(result); // NaN
Hoặc kết quả của phép chia 0.0 / 0.0:
double result = 0.0 / 0.0;
System.out.println(result); // NaN
NaN là "mọi thứ mà trong đời thực không phải là số".
Kiểm tra các giá trị đặc biệt
Trong Java có các hàm để kiểm tra các giá trị đặc biệt:
System.out.println(Double.isInfinite(result)); // true nếu là vô cực
System.out.println(Double.isNaN(result)); // true nếu là NaN
Bảng: double phản ứng thế nào với các phép toán bất thường
| Phép toán | Kết quả | double lưu gì |
|---|---|---|
|
Infinity | +∞ |
|
-Infinity | -∞ |
|
NaN | Không phải số |
|
NaN | Không phải số |
5. Các lỗi thường gặp khi làm việc với số thực
Lỗi số 1: So sánh số thực bằng ==
Bẫy phổ biến nhất: cố kiểm tra hai số dấu phẩy động tính toán ra có bằng nhau không bằng phép so sánh thông thường. Do lỗi làm tròn tích lũy, bạn gần như luôn nhận kết quả bất ngờ. Luôn dùng so sánh với ngưỡng sai số (epsilon).
Lỗi số 2: NaN và Infinity bất ngờ trong tính toán
Nếu không kiểm soát việc chia cho 0 hoặc căn của số âm, chương trình có thể xuất hiện NaN hoặc Infinity và "lây" sang mọi phép tính phía sau. Đừng quên kiểm tra các giá trị đáng ngờ bằng Double.isNaN() và Double.isInfinite() — điều này giúp tránh những bất ngờ khó chịu.
Lỗi số 3: Dùng NaN như "cờ báo"
Một số người mới dùng NaN như giá trị "đặc biệt" để kiểm tra, ví dụ "nếu không tìm thấy — trả về NaN". Nhưng hãy nhớ: NaN rất "khó chịu", và so sánh bằng == sẽ không hiệu quả! Chỉ kiểm tra bằng các phương thức chuyên dụng.
Lỗi số 4: Mong đợi ngoại lệ khi chia cho 0.0
Khác với phép chia số nguyên, chia cho 0.0 với double không ném lỗi mà trả về Infinity hoặc NaN. Điều này có thể dẫn đến bug "im lặng" khó phát hiện — vẫn có kết quả, nhưng không như bạn mong muốn.
GO TO FULL VERSION