当前位置: 首页>>编程语言>>正文


Java BigDecimal浮点数运算–如何保证运算精度不溢出

加减乘除四则运算是高级程序设计语言(不论机器语言、汇编还是其他高级语言)最基础的部分,Java作为最流行的软件开发语言之一,涉及四则运算的程序代码和功能业务随处可见。在笔者从事的基于Java语言银行、电商等软件系统开发过程中,涉及数字和金钱的业务比较多,也遇到了各种数据精度问题,下面从几个小例子,看看如何正确使用Java中的数据类型合理进行四则运算,保证数据精度。

1.整型计算

可能有人会说,整型数据肯定不会有计算错误和精度丢失问题。确实是这样吗?
首先请看代码:

int a = 345679;
int b = 8989;
int c = a*b;    // c的结果:-1187658765  显然是错误的

是不是和大家想的不一样,那么接着看下面的代码:

long a = 345679;
int b = 8989;
long c = a*b;   // c的结果为:3107308531  正确

当然,这种问题对于理解了整型int、long的数据长度限制的程序员来说,再简单不过了。这里只是举个例子,说明在程序设计语言中,整型数据类型的长度是有位数限制的,再使用四则运算时是避免数据类型越界的问题,特别是加法、乘法运算。

2.Float/double浮点数计算

说到浮点类型,很多人会想到,float类型在做乘法、除法运算时会导致精度溢出。举例如下:

float a = 1.19f;
float b = 0.93f;
float c = a * b;   //c的结果为:1.1067001 误差 0.0000001
float aa = 2.94f;
float bb = 0.7f;
float d = aa / bb;  // c的结果为:4.2000003 误差0.000003

很明显,float类型数据乘除法确实会导致精度异常,所以大家在高精度要求的业务中,尽量不要使用float类型进行乘除运算,特别是涉及到money等敏感数据时。

那么float仅仅只有乘法与除法有误差吗?接下看一下例子:

float a = 1.89f;
float b = 1.61f;
float c = a - b;    //c的结果为:0.27999997  误差0.00000003
float aa = 3.891f;
float bb = 1.19f;
float cc = aa + bb; //cc的结果:5.0810003  误差0.0000003

类似地,更高精度类型double和float一样,在加减乘除运算上同样存在精度丢失异常问题,客观上理论上来说,float/double浮点数对要求很高精度计算中是不适用的。Java语言中最适合高精度计算的类型是什么呢,根据笔者多年的经验与遇到的精度异常问题,java.math.BigDecimal是首选,可以处理任意精度的加减乘除四则运算。

3.BigDecimal浮点计算

BigDecimal官方文档(摘录如下):

1. Immutable, arbitrary-precision signed decimal numbers.
2. For all arithmetic operators , the operation is carried out as though an exact intermediate result were first calculated and then rounded to the number of digits specified by the precision setting (if necessary), using the selected rounding mode.
Besides a logical exact result, each arithmetic operation has a preferred scale for representing a result. The preferred scale for each operation is listed in the table below.
3.These scales are the ones used by the methods which return exact arithmetic results; except that an exact divide may have to use a larger scale since the exact result may have more digits.

  1. BigDecimal是不可变的,可表示任意精度的有符号的十进制数字。

  2. 对所有的运算,BigDecimal计算操作会先返回一个精确的中间结果,然后根据设置的精度要求进行数据精度取舍(比如四舍五入),否则默认使用首选精度。除了一个逻辑精确的结果,加减乘除每种运算操作都具有表示结果的首选精度范围。(加减乘除首选精度以下图表):

Operation Preferred Scale of Result
Add(加) max(addend.scale(), augend.scale())
Subtract(减) max(minuend.scale(), subtrahend.scale())
Multiply(乘) multiplier.scale() + multiplicand.scale()
Divide(除) dividend.scale() – divisor.scale()
  1. 加减乘除运算方法都基于这些首选精度返回运算结果,除非精确的除法运算必须使用更大的精度,因为除法运算结果可能有更多的为位数。比如1/32等于0.03125。

既然BigDecimal可以准确保留运算精度,那么接着用实例验证,是否BigDecimal运算就不会出现精度异常。

3.1 float运算:

float a1 = 1.6f;
float b1 = 2.7f;
BigDecimal a = new BigDecimal(a1);
BigDecimal b = new BigDecimal(b1);
BigDecimal c = a.multiply(b);// c的结果:4.320000140666962806790252216160297393798828125 
BigDecimal d = a.add(b);  // d的结果:4.30000007152557373046875

3.2 double运算:

double a1 = 1.6;
double b1 = 2.7;
BigDecimal a = new BigDecimal(a1);
BigDecimal b = new BigDecimal(b1);
BigDecimal c = a.multiply(b); //c的结果:4.32000000000000052402526762307390285717225175910329573457130565572459346412870218046009540557861328125
BigDecimal d = a.add(b);  //d的结果:4.300000000000000266453525910037569701671600341796875

从以上数据结果可以看出,float、double虽然经过BigDecimal封装,但是运算结果同样存在误差,那么怎么才能保证运算的准确行呢?通过一些实际使用与试验验证的经验,推荐方法是使用BigDecimal构造函数BigDecimal(String),将基本数据类型float/double型数据转成String,然后通过String创建BigDecimal实例进行运算。针对此构造函数,官方说明如下:

Note: For values other than float and double NaN and ±Infinity, this constructor is compatible with the values returned by Float.toString(float) and Double.toString(double). This is generally the preferred way to convert a float or double into a BigDecimal, as it doesn’t suffer from the unpredictability of the BigDecimal(double) constructor.

使用BigDecimal(String)构造函数将float/double转换为BigDecimal,这样做也不会遇到类似BigDecimal(double)构造函数的不可预测性。
同样地,继续来用例子证实BigDecimal(String)的运算准确性,为了对比,使用3.1、3.2相同的数据

3.3 更精确的float运算

float a1 = 1.6f;
float b1 = 2.7f;
BigDecimal a = new BigDecimal(String.valueOf(a1));   // 构造函数参数为String
BigDecimal b = new BigDecimal(String.valueOf(b1));
BigDecimal c = a.multiply(b);  // c的结果:4.32
BigDecimal d = a.add(b);   // d的结果:4.3

3.4 更精确的double运算

double a1 = 1.6;
double b1 = 2.7;
BigDecimal a = new BigDecimal(String.valueOf(a1));
BigDecimal b = new BigDecimal(String.valueOf(b1));
BigDecimal c = a.multiply(b);   // c的结果:4.32 
BigDecimal d = a.add(b);  // d的结果:4.3

最后,经过前后几组数据结果对比,进一步证实了BigDecimal(String)能更好的保证加减乘除的运算结果的准确性。

本文由《纯净天空》出品。文章地址: https://vimsky.com/article/3417.html,未经允许,请勿转载。