The problem of losing the precision of Java floating point arithmetic


Today, the teacher asked a question: how much is 9.8 - 0.1? It's a very simple question, but it hides a very big problem. If you don't pay attention, step on the pit. The code is as follows

double a = 9.8;
double b = 0.1;
System.out.println(a+b); // 9.9
System.out.println(a-b); // 9.700000000000001
System.out.println(a*b); // 0.9800000000000001
System.out.println(a/b); // 9.9

Why isn't the subtraction answer 9.7? This involves the underlying computing principle of the computer

Data exists in binary form in computer memory. Decimal numbers can be accurately converted when converted into binary numbers, but floating-point numbers can't, for example

0.98 = 0.111110101110000101000111101011100001010001111010111

Therefore, there is a high probability that the precision of floating-point numbers will be lost during conversion, which is why 9.8 - 0.1= nine point seven

So after knowing this pit, how can we avoid this problem?

Java provides us with a class for this purpose


1. Introduction

The API class BigDecimal provided by Java in the java.math package is used to accurately calculate the number of significant bits exceeding 16 bits. Double, a double precision floating-point variable, can handle 16 bit significant numbers. In practical applications, larger or smaller numbers need to be calculated and processed. float and double can only be used for scientific calculation or engineering calculation. java.math.BigDecimal should be used in business calculation. BigDecimal creates objects. We cannot use traditional arithmetic operators such as +, -, *, / to directly perform mathematical operations on their objects, but must call their corresponding methods. The parameter in the method must also be an object of BigDecimal. Constructors are special methods of classes that are specifically used to create objects, especially objects with parameters.

2. Construction method

BigDecimal(int)    // Creates an object with the integer value specified by the parameter
BigDecimal(double) // Creates an object with the double value specified by the parameter   (not recommended)
BigDecimal(long)   // Creates an object with the long integer value specified by the parameter
BigDecimal(String) // Create an object with the value specified by the parameter as a string (recommended)

3. Some methods

add(BigDecimal)      // Add
subtract(BigDecimal) // subtract 
multiply(BigDecimal) // Multiply
divide(BigDecimal)   // be divided by

4. Simple use

double a = 9.8;
double b = 0.1;
BigDecimal x = new BigDecimal(Double.toString(a)); // Constructor using String type
BigDecimal y = new BigDecimal(Double.toString(b));
System.out.println(x.add(y));      // 9.9
System.out.println(x.subtract(y)); // 9.7
System.out.println(x.multiply(y)); // 0.98
System.out.println(x.divide(y));   // 9.7

You can see that Java encapsulates such a useful class for us. Later, when we need to operate on floating-point numbers, we just need to use it directly

5. Some issues

1. Why is the double type construction method not recommended

Let's look at the code first

double a = 9.8;
double b = 0.1;
BigDecimal x = new BigDecimal(a); // Constructor using double type
BigDecimal y = new BigDecimal(b);
System.out.println(x); // 9.800000000000000710542735760100185871124267578125
System.out.println(y); // 0.1000000000000000055511151231257827021181583404541015625

It can be seen that the problem of precision loss occurs when initializing the object, and the corresponding explanation is given in the API document

1. The result of this constructor may be somewhat unpredictable. Suppose you write new BigDecimal(0.1) in Java to create a BigDecimal, which is completely equal to 0.1 (non scale value is 1, scale is 1), but actually equal to 0.10000000000005551231257827021181583404541015625. This is because 0.1 cannot be represented exactly like double (or as any binary fraction of finite length). Therefore, the value being passed to the construct is not exactly equal to 0.1, although on the surface.

two   The String construction, on the other hand, is completely predictable: write new BigDecimal("0.1") to create BigDecimal, which is exactly equal to 0.1, as expected. Therefore, it is generally recommended that String constructor take precedence over this.

three   When double must be used as the source for BigDecimal, note that this construct provides an accurate conversion; It does not convert double to String, using the Double.toString(double) method and then using the BigDecimal(String) constructor to get the same result. To get this result, use the static valueOf(double) method.

  Therefore, in order to avoid this situation, it is recommended to use the construction method of String type for initialization

2. Possible pit in division operation

BigDecimal division may not be divisible, such as 4.5 / 1.3. In this case, java.lang.arithmetexception: non terminating decimal expansion will be reported; no exact representable decimal result.

In fact, the divide method can pass three parameters: public BigDecimal divide (BigDecimal division, int scale, int rounding mode). The first parameter represents the divisor, the second parameter represents the number of digits reserved after the decimal point, and the third parameter represents the rounding mode. The rounding mode is only used in Division or rounding. There are the following types

// x / y, keep 4 digits after the decimal point, rounding mode
System.out.println(x.divide( y ,4, RoundingMode.CEILING) );     // Round to positive infinity
System.out.println(x.divide( y ,4, RoundingMode.DOWN) );        // Round to 0
System.out.println(x.divide( y ,4, RoundingMode.FLOOR) );       // Round to negative infinity
System.out.println(x.divide( y ,4, RoundingMode.HALF_DOWN) );   // Round to the nearest side. Both sides are the same. Round down
System.out.println(x.divide( y ,4, RoundingMode.HALF_EVEN) );   // Round to the nearest side. Both sides are the same. If the reserved bit is an odd number, it will go up and vice versa
System.out.println(x.divide( y ,4, RoundingMode.HALF_UP) );     // Round to the nearest side, both sides are the same, round up
System.out.println(x.divide( y ,4, RoundingMode.UP) );          // Rounding away from 0
System.out.println(x.divide( y ,4, RoundingMode.UNNECESSARY) ); // The calculation result is accurate and does not need to be rounded. If the division is not complete, an error will still be reported

reference material

1. Baidu Encyclopedia:   BigDecimal_ Baidu Encyclopedia

Tags: Java

Posted on Wed, 20 Oct 2021 13:57:09 -0400 by nediaz