99 Ngày Java — Ngày 27: Math, Random & BigDecimal
Ngày 26 làm chủ thời gian — hôm nay ta làm chủ những con số, và bắt đầu bằng cú sốc kinh điển: 0.1 + 0.2 không bằng 0.3. Đó không phải bug mà là bản chất của số thực nhị phân — và hiểu nó quyết định bạn có dám để double giữ tiền hay không (gợi ý: không bao giờ). Bài hôm nay đi qua bẫy số thực, bộ công cụ Math với những góc ít ai để ý, ranh giới sống còn giữa Random và SecureRandom, và BigDecimal — kiểu dữ liệu xứng đáng đứng tên trên mọi hóa đơn.

Bẫy số thực — vì sao 0.1 + 0.2 không bằng 0.3
System.out.println(0.1 + 0.2); // 0.30000000000000004 ?!
0.1 + 0.2 == 0.3; // false
// Không phải bug của Java — là bản chất của số thực nhị phân (IEEE 754):
// 0.1 trong hệ NHỊ PHÂN là số vô hạn tuần hoàn 0.0001100110011...
// double chỉ giữ được 64 bit → phải CẮT — sai số sinh ra từ đó,
// rồi mỗi phép cộng nhân lại khuếch đại thêm.
// Hệ quả thực tế:
double tien = 0;
for (int i = 0; i < 10; i++) tien += 0.1;
tien == 1.0; // false! — 0.9999999999999999
// So sánh double đúng cách — sai số cho phép (epsilon):
Math.abs(a - b) < 1e-9; // thay cho a == b
// Quy tắc sắt: double cho ĐO LƯỜNG (khoảng cách, tỉ lệ, tọa độ) —
// TUYỆT ĐỐI không cho TIỀN. Tiền dùng gì? Cuối bài.Gốc rễ nằm ở hệ đếm:
0.1gọn gàng trong hệ thập phân nhưng là số vô hạn tuần hoàn trong hệ nhị phân — hệt như 1/3 thành 0.333… trong hệ mười;double(Ngày 3) chỉ có 64 bit nên buộc phải cắt, và phần bị cắt chính là sai số.Sai số tích lũy: cộng 0.1 mười lần không ra 1.0 — nên so sánh bằng
==trên double gần như luôn sai ý định; dùng epsilon (Math.abs(a-b) < 1e-9) khi buộc phải so double.Phân vai rõ ràng:
doublecho đo lường — tọa độ, tỉ lệ, khoảng cách, nơi sai số một phần tỷ vô hại; còn tiền, thuế, tỉ giá — nơi từng đồng lệch là sự cố kế toán — thuộc vềBigDecimalở cuối bài.
Math — quen mặt nhưng có góc khuất
// Math — bộ hàm static quen mặt (import sẵn, không cần new):
Math.max(a, b); Math.min(a, b); Math.abs(-5); // 5
Math.pow(2, 10); // 1024.0 (double!)
Math.sqrt(144); // 12.0
Math.round(2.5); // 3 — làm tròn NỬA LÊN theo số học thông thường
Math.floor(-1.5); // -2.0 — sàn luôn đi XUỐNG
Math.ceil(-1.5); // -1.0 — trần luôn đi LÊN
// Bẫy số âm của phép chia lấy dư:
-7 % 3; // -1 — dấu theo SỐ BỊ CHIA
Math.floorMod(-7, 3); // 2 — luôn không âm khi chia dương
// floorMod là lựa chọn đúng cho: index vòng tròn, ngày trong tuần, phân bucket
// Tràn số im lặng (Ngày 3) — Math cho công cụ FAIL-FAST:
int big = Integer.MAX_VALUE;
big + 1; // -2147483648 — tràn IM LẶNG, không báo gì
Math.addExact(big, 1); // ArithmeticException — nổ ngay, lỗi lộ sớm
// addExact / subtractExact / multiplyExact — đáng dùng ở mọi phép tính tiền, số lượngBẫy
%với số âm bắt nhiều người:-7 % 3ra-1chứ không phải2— index vòng tròn kiểu(i - 1) % nvì thế nổArrayIndexOutOfBoundsExceptionkhi i về 0;Math.floorModlà câu trả lời đúng.Tràn số của
int(Ngày 3) im lặng tuyệt đối — họaddExact/multiplyExactđổi im lặng lấy fail-fast: triết lý quen thuộc của series, lỗi nổ sớm luôn rẻ hơn dữ liệu sai lan xa.Chi tiết đáng nhớ:
Math.round(2.5)ra 3 nhưngMath.round(-2.5)ra-2(làm tròn nửa lên theo trục số) — khác vớiRoundingMode.HALF_UPcủa BigDecimal vốn đối xứng qua số 0; đừng để hai định nghĩa "làm tròn" lẫn nhau.
Random, ThreadLocalRandom & SecureRandom
// Random — giả ngẫu nhiên có SEED: cùng seed, cùng dãy số
Random r = new Random(42);
r.nextInt(100); // luôn cùng kết quả với seed 42 — quà cho TEST
// Cách hiện đại (Java 17+): giao diện RandomGenerator
RandomGenerator gen = RandomGenerator.getDefault();
gen.nextInt(1, 7); // xúc xắc 1-6 — chặn dưới GỘP, chặn trên LOẠI
// Đa luồng: ThreadLocalRandom — mỗi thread một nguồn, không tranh nhau
ThreadLocalRandom.current().nextInt(0, 100);
// Nhưng BẢO MẬT thì khác hẳn:
// Random đoán được — biết vài số đầu ra là suy ngược được seed!
// OTP, token đặt lại mật khẩu, mã mời → SecureRandom:
SecureRandom sr = new SecureRandom(); // nguồn entropy của hệ điều hành
byte[] token = new byte[32];
sr.nextBytes(token);
// chậm hơn Random — cái giá xứng đáng cho thứ kẻ tấn công muốn đoánRandomlà giả ngẫu nhiên: thuật toán tất định chạy từ seed — nghe như khuyết điểm nhưng là tính năng cho test: cố định seed là tái hiện được đúng kịch bản "ngẫu nhiên" gây lỗi.Quy ước chặn biên đáng khắc cốt:
nextInt(1, 7)cho 1–6, gộp dưới loại trên — nhất quán vớisubstring(Ngày 9) và mọi API phạm vi của Java; xúc xắc viếtnextInt(1, 7)chứ không phải(1, 6).Ranh giới bảo mật là tuyệt đối: đầu ra
Randomsuy ngược được seed — token đặt lại mật khẩu sinh bằng nó là lỗ hổng thật từng có tên CVE; mọi giá trị mà kẻ tấn công muốn đoán phải sinh từSecureRandom, chậm hơn nhưng không thể dự đoán.
BigDecimal — kiểu dữ liệu của tiền
// BigDecimal — số THẬP PHÂN chính xác tùy ý: kiểu xứng đáng giữ tiền
BigDecimal gia = new BigDecimal("19.99"); // từ CHUỖI — chính xác tuyệt đối
new BigDecimal(0.1); // ✗ 0.1000000000000000055511... — double đã hỏng sẵn!
BigDecimal.valueOf(0.1); // ✓ đi qua chuỗi "0.1" — lối tắt an toàn
// Cộng trừ nhân: immutable — trả về bản mới (quen chưa?)
BigDecimal tong = gia.multiply(BigDecimal.valueOf(3)); // 59.97
// CHIA phải khai báo làm tròn — không thì 1/3 nổ ArithmeticException:
BigDecimal motPhanBa = BigDecimal.ONE
.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP); // 0.33
// Tiền tệ: HALF_EVEN — "làm tròn chủ ngân hàng", không thiên vị hướng nào
gia.setScale(0, RoundingMode.HALF_EVEN);
// Bẫy cuối: equals so cả SCALE — compareTo mới so giá trị
new BigDecimal("2.0").equals(new BigDecimal("2.00")); // false!
new BigDecimal("2.0").compareTo(new BigDecimal("2.00")); // 0 — bằng nhau
// → so sánh BigDecimal: LUÔN compareTo (và cẩn thận khi làm key HashMap — Ngày 24)Khởi tạo quyết định tất cả:
new BigDecimal(0.1)kế thừa nguyên sai số của double (đã hỏng từ trước khi BigDecimal kịp nhìn thấy), cònnew BigDecimal("0.1")vàvalueOfđi từ dạng thập phân — luôn khởi tạo từ chuỗi.Phép chia bắt khai báo
scale+RoundingModelà thiết kế có chủ đích: 1/3 không biểu diễn hữu hạn được, và làm tròn tiền là quyết định nghiệp vụ —HALF_EVEN(làm tròn chủ ngân hàng) không thiên vị hướng nào nên là chuẩn của xử lý tài chính.Cặp
equals/compareTolệch nhau —2.0khôngequals2.00vì khác scale — là ngoại lệ hiếm hoi của contract Ngày 24: so sánh giá trị luôn dùng compareTo, và cân nhắc kỹ trước khi cho BigDecimal làm key củaHashMap.
Bài tập nhỏ
In
0.1 + 0.2, rồi cộng 0.1 một nghìn lần và so với 100.0 — đo sai số tích lũy bằngMath.abs.Viết
int chiSoVong(int i, int n)trả về index vòng tròn đúng với i âm — so%vàfloorModtrên i = -1.Sinh chuỗi token 32 byte bằng
SecureRandom+HexFormat— rồi giải thích vì saonew Random(42)không được phép làm việc này.Tính hóa đơn: 3 món giá
"19.99", giảm 10%, VAT 8% — làm tròn 2 chữ sốHALF_EVENtừng bước bằng BigDecimal; làm lại bằng double và so kết quả.Kiểm chứng bẫy scale: đặt
new BigDecimal("2.0")và"2.00"vàoHashSet— set có mấy phần tử? Vì sao?
Kết luận
Bốn ý mang về: sai số của double là bản chất nhị phân, không phải bug — đo lường thì được, tiền thì không; Math có hai công cụ bị bỏ quên là floorMod cho số âm và họ addExact chống tràn im lặng; Random tất định theo seed — quà cho test, thảm họa cho bảo mật, nơi SecureRandom là bắt buộc; và BigDecimal giữ tiền với ba kỷ luật — khởi tạo từ chuỗi, chia phải khai báo làm tròn, so sánh bằng compareTo. Ngày 28 ta học ngôn ngữ tìm kiếm mẫu mạnh nhất của lập trình: Regex — Pattern, Matcher và những biểu thức đọc như mật mã nhưng tiết kiệm hàng trăm dòng code. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


