Backend

99 Ngày Java — Ngày 27: Math, Random & BigDecimal

SSite Admin
23 tháng 08, 2026 8 phút đọc 12 lượt xem
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.

Sketchnote Ngày 27: Math, Random và BigDecimal — bẫy 0.1 + 0.2, floorMod và addExact, Random vs SecureRandom, và BigDecimal với RoundingMode cho tiền tệ

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.1 gọ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: double cho đ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ượng
  • Bẫy % với số âm bắt nhiều người: -7 % 3 ra -1 chứ không phải 2 — index vòng tròn kiểu (i - 1) % n vì thế nổ ArrayIndexOutOfBoundsException khi i về 0; Math.floorMod là 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ưng Math.round(-2.5) ra -2 (làm tròn nửa lên theo trục số) — khác với RoundingMode.HALF_UP củ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 RandomGeneratorRandomGenerator 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ànhbyte[] 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án
  • Random là 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ới substring (Ngày 9) và mọi API phạm vi của Java; xúc xắc viết nextInt(1, 7) chứ không phải (1, 6).

  • Ranh giới bảo mật là tuyệt đối: đầu ra Random suy 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ềnBigDecimal gia = new BigDecimal("19.99");     // từ CHUỖI — chính xác tuyệt đốinew 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òn new 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 + RoundingMode là 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/compareTo lệch nhau — 2.0 không equals 2.00 vì 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ủa HashMap.

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ằng Math.abs.

  • Viết int chiSoVong(int i, int n) trả về index vòng tròn đúng với i âm — so % và floorMod trên i = -1.

  • Sinh chuỗi token 32 byte bằng SecureRandom + HexFormat — rồi giải thích vì sao new 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_EVEN từ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ào HashSet — 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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 72: Spring Cloud Config

Cấu hình tập trung trong Git, Config Server và spring.config.import, refresh khi đang chạy với @RefreshScope và Spring Cloud Bus, mã hóa secret bằng {cipher}.

7 thg 10, 20267 phút5
99 Ngày Java — Ngày 72: Garbage Collection

Reachability từ GC root, minor và full GC, G1 mặc định và ZGC thế hệ trong Java 21, cách đọc log GC bằng -Xlog và những thói quen nên bỏ.

7 thg 10, 20269 phút4
99 Ngày Spring — Ngày 71: Microservices tổng quan

Monolith và microservices khác nhau ở đâu, được gì và trả giá gì, khi nào không nên tách, và vì sao nên bắt đầu bằng modular monolith với Spring Modulith.

6 thg 10, 20269 phút8