Những câu hỏi lập trình khi pv (2): bài toán ATM

Đối với những bạn học công nghệ thông tin chắc hẳn đã từng gặp một số bài toán như:

  • Đưa hàng bằng thang máy (bài toán thang máy).
  • Trả lại tiền thừa khi sử dụng các máy bán hàng tự động (bài toán máy ATM).
  • Bài toán tính diện tích, chu vi của một hình bất kỳ...

Và ngày hôm nay mình sẽ đề cập lại một trong số các vấn đề đó dưới một khía cạnh khác:

Bài toán máy bán hàng

Vấn đề như sau:

Nam đi tới RoyalCity để mua một lon Cocacola. Anh ta mua bằng một máy bán hàng tự động (không có người bán hàng). Lon nước ngọt có giá là $1.10. Tuy nhiên trong ví Nam chỉ có một tờ $2.0. Nam sẽ nhận được bao nhiêu tiền thừa nếu anh ta trả lon nước đó với tờ $2.0 trong túi?

Đây là một chương trình cố gắng giải quyết vấn đề trên. Theo bạn, chương trình sẽ in ra gì?

public class Atm {
    public static void main(String args[]){
        System.out.println(2.00 - 1.10);
    }
}

Thông thường bạn có thể nghĩ rằng chương trình sẽ in ra 0.90, nhưng làm sao chương trình có thể biết bạn muốn in ra 2 số sau dấu phảy động?

Nếu bạn đã đọc qua Java-Docs của Double.toString Java-API thì bạn sẽ biết rằng chương trình sẽ in ra phần thập phân ngắn nhất đủ để phân biệt với các số double khác, với ít nhất 1 số trước và 1 số sau dấu phảy động. Như vậy bạn có thể đoán rằng khi chạy chương trình sẽ in ra 0.9. Tuy nhiên thực tế lại không phải như vậy, chương trình sẽ in ra 0.8999999999999999

Vấn đề là số 1.10 không thể biểu diễn chính xác dưới dạng double. Vì vậy nó sẽ được biểu diễn dưới dạng một số double có giá trị gần nó nhất. Sau đó sẽ lấy 2 trừ đi giá trị này. Kết quả sẽ ra một số gần với 0.9. Mặc dù Double.toString() có thể sẽ lấy giá trị gần nhất (là 0.9) nhưng giá trị này lại không thể biểu diễn dưới dạng số kiểu double, mà kết quả chính là số bạn nhìn thấy ở trên.

Nói chung, vấn đề chính là: không phải tất cả các số thập phân có thể được biểu diễn chính xác dưới dạng nhị phân với dấu phảy động (floating-point).

Nếu bạn đang sử dụng Java 5.0 hay mới hơn thì bạn có thể sửa chương trình trên bằng việc sử dụng format như sau:

// Cách đơn giản - vẫn sử dụng dấu phảy động
System.out.println("%.2f%n", 2.00 - 1.10);

Cách trên nói chung là sẽ in ra đúng câu trả lời mong muốn nhưng không thể hiện đúng được bản chất xử lý ở bên dưới: vẫn sử dụng kiểu toán học double hay còn gọi là floating-point (có làm tròn và không thực sự chính xác - như đã nói ở trên). Dấu phảy động (floating-points) không thực sự thích hợp với các công việc liên quan tới tính toán tiền tệ, đồng nghĩa với việc để biểu diễn 0.1 là không thể (số nhị phân có hữu hạn chiều dài).

Một cách để giải quyết vấn đề này là sử dụng kiểu dữ liệu nguyên (primitive-type), chẳng hạn như int hay long và thực hiện các tính toán tới từng xu tiền lẻ. Nếu bạn đi theo hướng này thì cũng phải đảm bảo kiểu dữ liệu đó đủ lớn để lưu trữ được hết giá trị đầu vào và kết quả của việc tính toán mà bạn sử dụng trong chương trình. Đối với riêng bài toán lúc đầu thì số int là đủ để sử dụng.

Đoạn chương trình sau sẽ minh họa cho cách làm trên:

System.out.println((200-100)+ " xu");

Còn một cách khác để giải quyết vấn đề này (nhưng ít được để ý hơn) là sử dụng BigDecimal, class này sẽ thực hiện chính xác phép toán. Hơn nữa, nó cũng hoàn toàn tương thích với kiểu DECIMAL trong SQL thông qua JDBC.

Tuy nhiên có một chú ý nhỏ: Luôn sử dụng constructor BigDecimal(String), và đừng bao giờ sử dụng BigDecimal(double). Constructor thứ hai sẽ khai báo giá trị đúng bằng với tham số truyền vào. new BigDecimal(.1) trả về một BigDecimal có giá trị 0.1000000000000000000055511151231257827021181583404541015625.

Sử dụng BigDecimal đúng, chương trình sẽ in ra đúng kết quả mong muốn: 0.90:

import Java.math.BigDecimal;
public class Change {
    public static void main(String args[]) {
        System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10")));
    }
}

Java cung cấp cơ chế hỗ trợ xử lý tính toán phi ngôn ngữ thông qua class BigDecimal. Tuy nhiên việc tính toán qua BigDecimal cũng chậm hơn so với các kiểu dữ liệu nguyên (primitive type) khác. Việc này có thể ảnh hưởng tới những chương trình có việc tính toán nhiều.

Nói chung là nên tránh việc sử dụng floatdouble đối với những phép tính cần kết quả chính xác. Chẳng hạn như đối với việc tính tiền, nên sử dụng int, long hay BigDecimal.

Theo bạn chú voi bên dưới có bao nhiêu chân?

All Rights Reserved