Project-driven learning and practice-tested knowledge
foreword
Many systems have the requirement of "processing money", such as e-commerce systems, financial systems, cashier systems, and so on. As long as it has something to do with money, you have to treat it with 120,000 points of energy, and you can't make a single mistake, otherwise it will be a disaster for the system and users.
There are two main aspects to the accuracy of guaranteed amounts: overflow and precision . Overflow means that there is enough space to store data, and it cannot be stored if the amount is too large. Accuracy means that there should be no deviation when calculating the amount, and neither more nor less will work.
Everyone knows how to solve the overflow problem. Just choose a numeric type with a long number of digits, that is, don’t float
use it double
. The precision problem double
cannot be solved, because floating-point numbers will cause precision loss.
Let's intuitively feel the loss of precision:
double money = 1.0 - 0.9;
Everyone knows the result of this calculation 0.1
, but the actual result is 0.09999999999999998
. This phenomenon occurs because the bottom layer of the computer is binary operations, and binary cannot accurately represent decimal decimals. Therefore, in precise calculations such as commercial calculations, other data types must be used to ensure that the precision is not lost, and floating-point numbers must not be used.
Next, this crab will explain in detail how to perform commercial calculations in actual development, and put all the code and SQL statements on Github (the address is at the end of the article), and it can be run after being cloned.
solution
There are two data types that can meet the needs of business computing. The first is naturally the Decimal type specially designed for business computing , and the second is a fixed-length integer .
Decimal
Regarding the choice of data types, one must consider the database, and the other must consider the programming language. That is, what type is used to store data in the database , and what type is used to process data in the code .
At the database level, the type is naturally used decimal
, because there is no loss of precision in this type, and it is perfect for commercial calculations.
decimal
The syntax for defining a field as is decimal(M,N)
, M
represents how many digits to store, and N
represents how many decimal places to store. Assume decimal(20,2)
, it means that a total of 20 digits are stored, of which decimals occupy 2 digits.
We create a new user table with two simple fields, primary key and balance:
Here, 2 points are reserved for the decimal position, which means that the amount is only stored in cents , and the unit stored in the actual project depends on the business needs, which is all possible.
The database level is done. Let's look at the code level. In Java, the type corresponds to the database decimal
, java.math.BigDecimal
and it can naturally ensure that the precision is completely accurate.
BigDecimal
There are three main ways to create :
BigDecimal d1 = new BigDecimal(0.1); // BigDecimal(double val)
BigDecimal d2 = new BigDecimal("0.1"); // BigDecimal(String val)
BigDecimal d3 = BigDecimal.valueOf(0.1); // static BigDecimal valueOf(double val)
The first two are constructors, and the latter is a static method. All three methods are very convenient, but the first method is forbidden! Take a look at the print results of each of these three objects to see why:
d1: 0.1000000000000000055511151231257827021181583404541015625
d2: 0.1
d3: 0.1
In the first method, double
the parameters of the type passed in through the constructor cannot accurately obtain the value. If you want to create it correctly BigDecimal
, you must either double
convert it to a string and then call the constructor, or call the static method directly. In fact, inside the static method is also double
the constructor that will be converted to a string and then called:
If the decimal value is queried from the database, or the decimal value is passed from the front end, the data will be accurately mapped into BigDecimal
an object, so we don't have to worry about this.
After talking about creation, let's talk about the most important numerical operations. Operations are nothing more than addition, subtraction, multiplication and division, which BigDecimal
provide corresponding methods:
BigDecimal add(BigDecimal); // 加
BigDecimal subtract(BigDecimal); // 减
BigDecimal multiply(BigDecimal); // 乘
BigDecimal divide(BigDecimal); // 除
BigDecimal
It is an immutable object, which means that these operations will not change the value of the original object, and only a new object will be returned after the method is executed. If you want to update the original value after the operation, you can only reassign it:
d1 = d1.subtract(d2);
There is no proof, let's verify whether the accuracy will be lost:
BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("0.9");
System.out.println(d1.subtract(d2));
The output is undoubtedly 0.1
.
The code has been able to guarantee that the precision will not be lost, but the division in mathematics may be inexhaustible. For example, if we divide 10
by 3
, the following exception will be thrown:
In order to solve the problem of infinite decimals caused by indivisibility, we need to artificially control the precision of decimals. There is another method of division operation that is used to control precision:
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
scale
The parameter indicates how many decimal places are retained after the operation, and roundingMode
the parameter indicates the way to calculate the decimal.
BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("3");
System.out.println(d1.divide(d2, 2, RoundingMode.DOWN)); // 小数精度为2,多余小数直接舍去。输出结果为0.33
Enumeration can be used RoundingMode
to conveniently specify the decimal calculation method. In addition to direct rounding, there are also multiple methods such as rounding and rounding up, which can be specified according to specific business needs.
Note that the decimal precision should be controlled in the code as much as possible, not through the database. By default, the database uses rounding to preserve decimal precision.
For example, the decimal precision set in the database is 2,
0.335
and if I store it, the final stored value will become0.34
.
Now that we know how to create and manipulate BigDecimal
objects, there is only one last operation left: comparison. Because it is not a basic data type, ==
it is definitely not possible to use double equal signs, so let's try to use equals
comparison:
BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.equals(d2)); // false
The output result is false
, because BigDecimal
the equals
method not only compares the value, but also compares the precision, even if the value is the same but the precision is different false
. If you want to determine whether the values are the same, you need to use int compareTo(BigDecimal val)
the method:
BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.compareTo(d2) == 0); // true
d1
greater than d2
, return 1
;
d1
less than d2
, return -1
;
If the two values are equal, return 0
.
BigDecimal
The usage of is introduced here, let's look at the second solution next.
fixed-length integer
Fixed-length integers, as the name implies, are integers of fixed (decimal) length. It is just a concept, not a new data type, we still use ordinary integers.
Amounts seem to have decimals as a matter of course, but a little thought reveals that decimals are not necessary. The unit of the amount we demonstrated before is yuan , 1.55
which is one yuan and five cents. Then if our unit is angle , the value of one yuan, five cents and five cents will become 15.5
. If the unit is then narrowed down to minutes , the value is 155
. That's right, as long as the smallest unit is reached, decimals can be omitted! The minimum unit is determined according to business requirements. For example, if the system requires accuracy to centimeters , then the value is 1550
. Of course, it is generally accurate to the point, and the units we will demonstrate next are all points.
Let's create a new field now, the type is bigint
and the unit is points:
The corresponding data type in the code is naturally Long
. We are very familiar with the numerical operations of basic types, and we can use the operation operators directly:
long d1 = 10000L; // 100元
d1 += 500L; // 加五元
d1 -= 500L; // 减五元
There is nothing to say about addition and subtraction. There may be decimals in multiplication and division. For example, if a product is discounted by 20%, the operation is to multiply 0.8
:
long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 打八折,运算后结果为1892.8
d1 = (long)result; // 转换为整数,舍去所有小数,值为1892。即18.92元
For decimal operations, the type will naturally become a floating-point number, so we also need to convert the floating-point number to an integer.
Forced transfer will round off all decimals, this rounding does not mean loss of precision . What is the minimum unit of business requirements, we only keep what is, and we don't need to keep the units that are lower than the score. This point BigDecimal
is consistent with that. If only points are needed in the system, the decimal precision is 2
, and the remaining decimals are discarded.
However, some business calculations may require other operations such as rounding, which we can Math
do through classes:
long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 运算后结果为1892.8
d1 = (long)result; // 强转舍去所有小数,值为1892
d1 = (long)Math.ceil(result); // 向上取整,值为1893
d1 = (long)Math.round(result); // 四舍五入,值为1893
...
Let's look at the division operation. When dividing integers by integers, all decimals are automatically rounded off:
long d1 = 2366L;
long result = d1 / 3; // 正确的值本应该为788.6666666666666,舍去所有小数,最终值为788
If you want to perform other decimal operations such as rounding, perform floating-point operations first, and then convert to integers:
long d1 = 2366L;
double result = d1 / 3.0; // 注意,这里除以不是 3,而是 3.0 浮点数
d1 = (long)Math.round(result); // 四射勿入,最终值为789,即7.89元
Although both database storage and code operations are integers, if the front-end display is still in cents , it is not very user-friendly. So after the backend passes the value to the frontend, the frontend needs to divide the value by itself 100
and display it to the user in yuan . Then when the front end passes the value to the back end, it is still passed as an agreed integer.
ending
The processing of the amount is explained. We learned two business computing scenarios:
-
Decimal type
-
fixed-length integer
In fact, business computing is not technically difficult, but if it is not handled correctly, it will cause incalculable losses. After all, money-related matters are not trivial.