Thành thạo BigDecimal trong Java: Tính toán tiền tệ chính xác không lỗi dấu phẩy động

目次

1. Giới thiệu

Vấn đề độ chính xác trong tính toán số học bằng Java

Trong lập trình Java, các phép tính số học được thực hiện hàng ngày. Ví dụ, tính giá sản phẩm, xác định thuế hoặc lãi suất — những thao tác này cần thiết trong rất nhiều ứng dụng. Tuy nhiên, khi các phép tính này được thực hiện bằng các kiểu số thực như float hoặc double, lỗi không mong muốn có thể xuất hiện.

Điều này xảy ra vì floatdouble biểu diễn giá trị dưới dạng xấp xỉ nhị phân. Các giá trị như “0.1” hay “0.2”, có thể biểu diễn chính xác trong hệ thập phân, lại không thể biểu diễn một cách chính xác trong hệ nhị phân — và do đó, các sai lệch nhỏ sẽ tích lũy.

BigDecimal là cần thiết cho các phép tính tiền tệ hoặc độ chính xác cao

Những sai lệch này có thể trở nên nghiêm trọng trong các lĩnh vực như tính toán tiền tệ và các phép tính khoa học/kỹ thuật yêu cầu độ chính xác. Ví dụ, trong tính toán hoá đơn, chỉ một chênh lệch 1-yên cũng có thể gây ra vấn đề uy tín.

Đây là nơi lớp BigDecimal của Java tỏa sáng. BigDecimal có thể xử lý các số thập phân với độ chính xác tùy ý và khi thay thế float hoặc double bằng nó, các phép tính số học có thể được thực hiện mà không gặp lỗi.

Những gì bạn sẽ nhận được từ bài viết này

Trong bài viết này, chúng tôi sẽ giải thích các kiến thức cơ bản về cách sử dụng BigDecimal trong Java, các kỹ thuật nâng cao, cũng như các lỗi thường gặp và lưu ý cần tránh một cách có hệ thống.

Điều này hữu ích cho những ai muốn thực hiện các phép tính tiền tệ một cách chính xác trong Java hoặc đang cân nhắc áp dụng BigDecimal vào dự án của mình.

2. BigDecimal là gì?

Tổng quan về BigDecimal

BigDecimal là một lớp trong Java cho phép thực hiện các phép tính thập phân có độ chính xác cao. Nó thuộc gói java.math và được thiết kế đặc biệt cho các phép tính không chấp nhận lỗi như tính toán tài chính/kế toán/thuế.

Với floatdouble của Java, các giá trị số được lưu trữ dưới dạng xấp xỉ nhị phân — nghĩa là các số thập phân như “0.1” hay “0.2” không thể biểu diễn một cách chính xác, và đây chính là nguồn gốc của lỗi. Ngược lại, BigDecimal lưu trữ giá trị dưới dạng chuỗi thập phân, do đó giảm thiểu lỗi làm tròn và xấp xỉ.

Xử lý các số có độ chính xác tùy ý

Đặc điểm lớn nhất của BigDecimal là “độ chính xác tùy ý”. Cả phần nguyên và phần thập phân về lý thuyết đều có thể xử lý số chữ số gần như không giới hạn, tránh việc làm tròn hoặc mất chữ số do giới hạn độ dài.
Ví dụ, số lớn sau có thể được xử lý một cách chính xác:

BigDecimal bigValue = new BigDecimal("12345678901234567890.12345678901234567890");

Khả năng thực hiện các phép tính đồng thời giữ nguyên độ chính xác như vậy là sức mạnh chính của BigDecimal.

Các trường hợp sử dụng chính

BigDecimal được khuyến nghị trong các tình huống như:

  • Tính toán tiền tệ — tính lãi, tính thuế trong các ứng dụng tài chính
  • Xử lý số tiền hoá đơn / báo giá
  • Các phép tính khoa học/kỹ thuật yêu cầu độ chính xác cao
  • Các quy trình mà việc tích lũy lâu dài gây ra lỗi tích tụ

Ví dụ, trong hệ thống kế toán và tính lương — nơi một chênh lệch 1-yên có thể dẫn đến tổn thất lớn hoặc tranh chấp — độ chính xác của BigDecimal là thiết yếu.

3. Cách sử dụng cơ bản của BigDecimal

Cách tạo các đối tượng BigDecimal

Khác với các literal số thông thường, BigDecimal thường nên được khởi tạo từ một chuỗi. Điều này vì các giá trị được tạo từ double hoặc float có thể đã chứa sẵn lỗi xấp xỉ nhị phân.

Khuyến nghị (khởi tạo từ String):

BigDecimal value = new BigDecimal("0.1");

Tránh (khởi tạo từ double):

BigDecimal value = new BigDecimal(0.1); // may contain error

Cách thực hiện các phép toán

BigDecimal không thể dùng với các toán tử số học thông thường (+, -, *, /). Thay vào đó, phải sử dụng các phương thức chuyên biệt.

Cộng (add)

BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("2.3");
BigDecimal result = a.add(b); // 12.8

Trừ (subtract)

BigDecimal result = a.subtract(b); // 8.2

Nhân (multiply)

BigDecimal result = a.multiply(b); // 24.15

Phép chia (divide) và Chế độ làm tròn

Phép chia cần thận trọng. Nếu không chia hết, ArithmeticException sẽ xảy ra trừ khi chỉ định chế độ làm tròn.

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

Ở đây chúng ta chỉ định “2 chữ số thập phân” và “làm tròn lên nửa”.

Đặt Scale và Chế độ làm tròn với setScale

setScale có thể được dùng để làm tròn tới một số chữ số nhất định.

BigDecimal value = new Big BigDecimal("123.456789");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

Các giá trị RoundingMode phổ biến:

Mode NameDescription
HALF_UPRound half up (standard rounding)
HALF_DOWNRound half down
HALF_EVENBanker’s rounding
UPAlways round up
DOWNAlways round down

BigDecimal là bất biến

BigDecimalbất biến. Nghĩa là — các phương thức số học (add, subtract, v.v.) không thay đổi giá trị gốc — chúng trả về một thể hiện mới.

BigDecimal original = new BigDecimal("5.0");
BigDecimal result = original.add(new BigDecimal("1.0"));
System.out.println(original); // still 5.0
System.out.println(result);   // 6.0

4. Sử dụng nâng cao của BigDecimal

So sánh giá trị: Sự khác nhau giữa compareTo và equals

Trong BigDecimal, có hai cách để so sánh giá trị: compareTo()equals(), và chúng hoạt động khác nhau.

  • compareTo() chỉ so sánh giá trị số (bỏ qua scale).
  • equals() so sánh kèm scale (số chữ số thập phân).
    BigDecimal a = new BigDecimal("10.0");
    BigDecimal b = new BigDecimal("10.00");
    
    System.out.println(a.compareTo(b)); // 0 (values are equal)
    System.out.println(a.equals(b));    // false (scale differs)
    

Lưu ý: Đối với kiểm tra bằng nhau về số học — như bằng nhau tiền tệ — compareTo() thường được khuyến nghị.

Chuyển đổi từ/đến String

Trong nhập liệu người dùng và nhập khẩu tệp bên ngoài, việc chuyển đổi với kiểu String là phổ biến.

String → BigDecimal

BigDecimal value = new Big BigDecimal("1234.56");

BigDecimal → String

String str = value.toString(); // "1234.56"

Sử dụng valueOf

Java cũng có BigDecimal.valueOf(double val), nhưng nó cũng bên trong chứa lỗi của double, vì vậy việc tạo từ chuỗi vẫn an toàn hơn.

BigDecimal unsafe = BigDecimal.valueOf(0.1); // contains internal error

Độ chính xác và quy tắc làm tròn qua MathContext

MathContext cho phép bạn kiểm soát độ chính xác và chế độ làm tròn cùng lúc — hữu ích khi áp dụng các quy tắc chung cho nhiều phép tính.

MathContext mc = new MathContext(4, RoundingMode.HALF_UP);
BigDecimal result = new BigDecimal("123.4567").round(mc); // 123.5

Cũng có thể dùng trong phép tính:

BigDecimal a = new BigDecimal("10.456");
BigDecimal b = new BigDecimal("2.1");
BigDecimal result = a.multiply(b, mc); // 4-digit precision

Kiểm tra null và Khởi tạo an toàn

Các biểu mẫu có thể truyền giá trị null hoặc rỗng — mã bảo vệ là tiêu chuẩn.

String input = ""; // empty
BigDecimal value = (input == null || input.isEmpty()) ? BigDecimal.ZERO : new BigDecimal(input);

Kiểm tra Scale của BigDecimal

Để biết số chữ số thập phân, dùng scale():

BigDecimal value = new BigDecimal("123.45");
System.out.println(value.scale()); // 3

5. Các lỗi thường gặp và cách khắc phục

ArithmeticException: Mở rộng thập phân không kết thúc

Ví dụ lỗi:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b); // exception

Đây là “1 ÷ 3” — vì nó tạo ra một số thập phân không kết thúc, nếu không chỉ định chế độ làm tròn/scale, một ngoại lệ sẽ được ném.

Cách khắc phục: chỉ định scale + chế độ làm tròn

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // OK (3.33)

Lỗi khi tạo trực tiếp từ double

Passing a double directly may contain binary error already — producing unexpected values.

Ví dụ không tốt:

BigDecimal val = new BigDecimal(0.1);
System.out.println(val); // 0.100000000000000005551115123...

Đúng: Sử dụng String

BigDecimal val = new BigDecimal("0.1"); // exact 0.1

Lưu ý: BigDecimal.valueOf(0.1) sử dụng Double.toString() nội bộ, vì vậy nó “gần như giống” new BigDecimal("0.1") — nhưng chuỗi là an toàn 100%.

Hiểu lầm về equals do không khớp Scale

equals() so sánh scale, nó có thể trả về false ngay cả khi các giá trị về mặt số học là bằng nhau.

BigDecimal a = new BigDecimal("10.0");
BigDecimal b = new BigDecimal("10.00");

System.out.println(a.equals(b)); // false

Giải pháp: sử dụng compareTo() để so sánh số học

System.out.println(a.compareTo(b)); // 0

Kết quả không mong đợi do độ chính xác không đủ

Nếu sử dụng setScale mà không chỉ định chế độ làm tròn — có thể xảy ra ngoại lệ.

Ví dụ không tốt:

BigDecimal value = new BigDecimal("1.2567");
BigDecimal rounded = value.setScale(2); // exception

Giải pháp:

BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // OK

NumberFormatException Khi Giá Trị Đầu Vào Không Hợp Lệ

Nếu văn bản không hợp lệ không thể phân tích thành số được truyền vào (ví dụ: đầu vào người dùng / trường CSV), sẽ xảy ra NumberFormatException.

Giải pháp: sử dụng xử lý ngoại lệ

try {
    BigDecimal value = new BigDecimal(userInput);
} catch (NumberFormatException e) {
    // show error message or fallback logic
}

6. Ví Dụ Sử Dụng Thực Tế

Ở đây chúng tôi giới thiệu các kịch bản thực tế minh họa cách BigDecimal có thể được sử dụng trong thực tế. Đặc biệt trong các tính toán tài chính/kế toán/thuế, tầm quan trọng của việc xử lý số chính xác trở nên rõ ràng.

Xử lý số thập phân trong tính toán giá (Làm tròn phần thập phân)

Ví dụ: Tính giá bao gồm thuế tiêu thụ 10%

BigDecimal price = new BigDecimal("980"); // price w/o tax
BigDecimal taxRate = new BigDecimal("0.10");
BigDecimal tax = price.multiply(taxRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal total = price.add(tax);

System.out.println("Tax: " + tax);         // Tax: 98
System.out.println("Total: " + total);     // Total: 1078

Điểm chú ý:

  • Kết quả tính thuế thường được xử lý dưới dạng số nguyên, sử dụng setScale(0, RoundingMode.HALF_UP) để làm tròn.
  • double thường gây ra lỗi — nên dùng BigDecimal.

Tính toán giảm giá (% OFF)

Ví dụ: Giảm giá 20%

BigDecimal originalPrice = new BigDecimal("3500");
BigDecimal discountRate = new BigDecimal("0.20");
BigDecimal discount = originalPrice.multiply(discountRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal discountedPrice = originalPrice.subtract(discount);

System.out.println("Discount: " + discount);         // Discount: 700
System.out.println("After discount: " + discountedPrice); // 2800

Điểm chú ý: Các phép tính giảm giá không được mất độ chính xác.

Tính toán Đơn Giá × Số Lượng (Kịch bản Ứng dụng Kinh doanh Điển hình)

Ví dụ: 298.5 yên × 7 món

BigDecimal unitPrice = new BigDecimal("298.5");
BigDecimal quantity = new BigDecimal("7");
BigDecimal total = unitPrice.multiply(quantity).setScale(2, RoundingMode.HALF_UP);

System.out.println("Total: " + total); // 2089.50

Điểm chú ý:

  • Điều chỉnh làm tròn cho phép nhân có phần thập phân.
  • Quan trọng cho hệ thống kế toán / đặt hàng.

Tính lãi suất kép (Ví dụ tài chính)

Ví dụ: Lãi suất hàng năm 3% × 5 năm

BigDecimal principal = new BigDecimal("1000000"); // base: 1,000,000
BigDecimal rate = new BigDecimal("0.03");
int years = 5;

BigDecimal finalAmount = principal;
for (int i = 0; i < years; i++) {
    finalAmount = finalAmount.multiply(rate.add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
}

System.out.println("Sau 5 năm: " + finalAmount); // khoảng 1,159,274.41

Point:

  • Repeated calculations accumulate errors — BigDecimal avoids this.

Validation & Conversion of User Input

public static BigDecimal parseAmount(String input) {
    try {
        return new BigDecimal(input).setScale(2, RoundingMode.HALF_UP);
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO; // coi đầu vào không hợp lệ là 0
    }
}

Points:

  • Safely convert user-provided numeric strings.
  • Validation + error fallback improves robustness.

7. Summary

The Role of BigDecimal

In Java’s numeric processing — especially monetary or precision-required logic — the BigDecimal class is indispensable. Errors inherent in float / double can be dramatically avoided by using BigDecimal.

This article covered fundamentals, arithmetic, comparisons, rounding, error handling, and real-world examples.

Key Review Points

  • BigDecimal handles arbitrary-precision decimal — ideal for money and precision math
  • Initialization should be via string literal , e.g. new BigDecimal("0.1")
  • Use add() , subtract() , multiply() , divide() , and always specify rounding mode when dividing
  • Use compareTo() for equality — understand difference vs equals()
  • setScale() / MathContext let you finely control scale + rounding
  • Real business logic cases include money, tax, quantity × unit price etc.

For Those About to Use BigDecimal

Although “handling numbers in Java” looks simple — precision / rounding / numeric error problems always exist behind it. BigDecimal is a tool that directly addresses those problems — mastering it lets you write more reliable code.

At first you may struggle with rounding modes — but with real project usage, it becomes natural.

Next chapter is an FAQ section summarizing common questions about BigDecimal — useful for review and specific semantic searches.

8. FAQ: Frequently Asked Questions About BigDecimal

Q1. Why should I use BigDecimal instead of float or double?

A1.
Because float/double represent numbers as binary approximations — decimal fractions cannot be represented exactly. This causes results such as “0.1 + 0.2 ≠ 0.3.”
BigDecimal preserves decimal values exactly — ideal for money or precision-critical logic.

Q2. What is the safest way to construct BigDecimal instances?

A2.
Always construct from string.
Bad (error):

new BigDecimal(0.1)

Correct:

new BigDecimal("0.1")

BigDecimal.valueOf(0.1) uses Double.toString() internally, so it’s almost same — but string is the safest.

Q3. Why does divide() throw an exception?

A3.
Because BigDecimal.divide() throws ArithmeticException when result is a non-terminating decimal.
Solution: specify scale + rounding mode

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);

Q4. What’s the difference between compareTo() and equals()?

A4.

  • compareTo() checks numeric equality (scale ignored)
  • equals() checks exact equality including scale
    new BigDecimal("10.0").compareTo(new BigDecimal("10.00")); // → 0
    new BigDecimal("10.0").equals(new BigDecimal("10.00"));    // → false
    

Q5. How do I perform rounding?

A5.
Use setScale() with explicit rounding mode.

BigDecimal value = new BigDecimal("123.4567");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

Main rounding modes:

  • RoundingMode.HALF_UP (round half up)
  • RoundingMode.DOWN (round down)
  • RoundingMode.UP (round up)

Q6. Can I check decimal digits (scale)?

A6.
Yes — use scale().

BigDecimal val = new BigDecimal("123.45");
System.out.println(val.scale()); // → 3

Q7. How should I handle null/empty input safely?

A7.
Luôn bao gồm kiểm tra null + xử lý ngoại lệ.

public static BigDecimal parseSafe(String input) {
    if (input == null || input.trim().isEmpty()) return BigDecimal.ZERO;
    try {
        return new BigDecimal(input.trim());
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO;
    }
}