概述
欢迎关注本人公众号
异常再现
大家应该都已经知道,涉及到金钱的计算应该使用BigDecimal,没有使用BigDecimal的已经被开除。
但是使用了BigDecimal后计算结果就一定是精确的吗?未必。
看下面测试
BigDecimal a = new BigDecimal(1.01);
BigDecimal b = new BigDecimal(2.02);
BigDecimal c = new BigDecimal("1.01");
BigDecimal d = new BigDecimal("2.02");
BigDecimal e = new BigDecimal(Double.toString(1.01));
BigDecimal f = new BigDecimal(Double.toString(2.02));
BigDecimal g = BigDecimal.valueOf(1.01);
BigDecimal h = BigDecimal.valueOf(2.02);
System.out.println(a.add(b));
System.out.println(c.add(d));
System.out.println(e.add(f));
System.out.println(g.add(h));
输出结果为:
3.0300000000000000266453525910037569701671600341796875
3.03
3.03
3.03
可以看到第一行输出是有问题的。
float和double精度问题
在java中,double是双精度,64位,浮点数,默认是0.0d。float是单精度,32位,浮点数,默认是0.0f;
其中float的存储方式如下图所示:
而双精度的存储方式为:
float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。
float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;
double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。
为什么会精度丢失?
计算机在处理数据都涉及到数据的转换和各种复杂运算,比如,不同单位换算,不同进制(如二进制十进制)换算等,很多除法运算不能除尽,比如10÷3=3.3333…无穷无尽,而精度是有限的,3.3333333x3并不等于10,经过复杂的处理后得到的十进制数据并不精确,精度越高越精确。float和double的精度是由尾数的位数来决定的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。float:2^23 = 8388608,一共七位,由于最左为1的一位省略了,这意味着最多能表示8位数: 28388608 = 16777216 。有8位有效数字,但绝对能保证的为7位,也即float的精度为7~8位有效数字;double:2^52 = 4503599627370496,一共16位,同理,double的精度为16~17位。
当到达一定值自动开始使用科学计数法,并保留相关精度的有效数字,所以结果是个近似数,并且指数为整数。在十进制中小数有些是无法完整用二进制表示的。所以只能用有限位来表示,从而在存储时可能就会有误差。对于十进制的小数转换成二进制采用乘2取整法进行计算,取掉整数部分后,剩下的小数继续乘以2,直到小数部分全为0。
BigDecimal分析
BigDecimal(double val)
这个方法是无法保证精度的,源码注释中也已经写明:
/**
* Translates a {@code double} into a {@code BigDecimal} which
* is the exact decimal representation of the {@code double}'s
* binary floating-point value. The scale of the returned
* {@code BigDecimal} is the smallest value such that
* <code>(10<sup>scale</sup> × val)</code> is an integer.
* <p>
* <b>Notes:</b>
* <ol>
* <li>
* The results of this constructor can be somewhat unpredictable.
* One might assume that writing {@code new BigDecimal(0.1)} in
* Java creates a {@code BigDecimal} which is exactly equal to
* 0.1 (an unscaled value of 1, with a scale of 1), but it is
* actually equal to
* 0.1000000000000000055511151231257827021181583404541015625.
* This is because 0.1 cannot be represented exactly as a
* {@code double} (or, for that matter, as a binary fraction of
* any finite length). Thus, the value that is being passed
* <em>in</em> to the constructor is not exactly equal to 0.1,
* appearances notwithstanding.
*
* <li>
* The {@code String} constructor, on the other hand, is
* perfectly predictable: writing {@code new BigDecimal("0.1")}
* creates a {@code BigDecimal} which is <em>exactly</em> equal to
* 0.1, as one would expect. Therefore, it is generally
* recommended that the {@linkplain #BigDecimal(String)
* String constructor} be used in preference to this one.
*
* <li>
* When a {@code double} must be used as a source for a
* {@code BigDecimal}, note that this constructor provides an
* exact conversion; it does not give the same result as
* converting the {@code double} to a {@code String} using the
* {@link Double#toString(double)} method and then using the
* {@link #BigDecimal(String)} constructor. To get that result,
* use the {@code static} {@link #valueOf(double)} method.
* </ol>
*
* @param val {@code double} value to be converted to
* {@code BigDecimal}.
* @throws NumberFormatException if {@code val} is infinite or NaN.
*/
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
并且告诉我们应该使用BigDecimal(String val)
:
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
当然使用BigDecimal valueOf(double val)
也是可以的,valueOf实际是先将double或flout的数据转为了string:
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
另外,特别注意,以下代码是错误的哦:
@Test
public void a(){
float a= 0.1f;
double b = a;
System.out.println(a);
System.out.println(b);
BigDecimal bigDecimal = BigDecimal.valueOf(0.1f);
System.out.println(bigDecimal);
}
输出:
0.1
0.10000000149011612
0.10000000149011612
其实这个不是BigDecimal.valueOf的问题,而是float转double时精度丢失了。
BigDecimal比较大小注意事项(equals)
在项目中使用BigDecimal的equals方法比较大小时,结果不为true,直接上示例
public static void main(String[] args) {
BigDecimal a = new BigDecimal(0.00);
BigDecimal b = new BigDecimal(0);
boolean result = a.equals(b);
System.out.println("a equals b -->" + result);
BigDecimal c = new BigDecimal("0.00");
BigDecimal d = new BigDecimal("0");
boolean result1 = c.equals(d);
System.out.println("c equals d -->" + result1);
}
结果:
a equals b -->true
c equals d -->false
可以看到a和b比较结果是true,c和d比较的结果为fasle
c、d使用传入字符串的构造器(等同于数据库查询出来的值)
项目中从数据库查询出来的值进行比较时(上例中c、d)显然不是我们期望的结果,因此修改为如下方法
boolean result2 = c.compareTo(d) == 0;
System.out.println("c compareTo d -->" + result2);
结果:
c compareTo d -->true
我们来看下构造器:
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
该构造器的注释中有这么一段:
* <p><b>Examples:</b><br>
* The value of the returned {@code BigDecimal} is equal to
* <i>significand</i> × 10<sup> <i>exponent</i></sup>.
* For each string on the left, the resulting representation
* [{@code BigInteger}, {@code scale}] is shown on the right.
* <pre>
* "0" [0,0]
* "0.00" [0,2]
* "123" [123,0]
* "-123" [-123,0]
* "1.23E3" [123,-1]
* "1.23E+3" [123,-1]
* "12.3E+7" [123,-6]
* "12.0" [120,1]
* "12.3" [123,1]
* "0.00123" [123,5]
* "-1.23E-12" [-123,14]
* "1234.5E-4" [12345,5]
* "0E+7" [0,-7]
* "-0" [0,0]
* </pre>
*
可以看出,“0”传入构造器得到的是0且没有小数位,“0.00”传入构造器得到的是0.00,含有2位小数
再看看equals方法:
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
可以清晰看到equals方法比较了小数位数 -----> if (scale != xDec.scale) return false;
到这里可以理解上面C、Dequals比较结果为什么是false了
再来看看compareTo方法
/**
* Compares this {@code BigDecimal} with the specified
* {@code BigDecimal}. Two {@code BigDecimal} objects that are
* equal in value but have a different scale (like 2.0 and 2.00)
* are considered equal by this method. This method is provided
* in preference to individual methods for each of the six boolean
* comparison operators ({@literal <}, ==,
* {@literal >}, {@literal >=}, !=, {@literal <=}). The
* suggested idiom for performing these comparisons is:
* {@code (x.compareTo(y)} <<i>op</i>> {@code 0)}, where
* <<i>op</i>> is one of the six comparison operators.
*
* @param val {@code BigDecimal} to which this {@code BigDecimal} is
* to be compared.
* @return -1, 0, or 1 as this {@code BigDecimal} is numerically
* less than, equal to, or greater than {@code val}.
*/
public int compareTo(BigDecimal val) {
// Quick path for equal scale and non-inflated case.
if (scale == val.scale) {
long xs = intCompact;
long ys = val.intCompact;
if (xs != INFLATED && ys != INFLATED)
return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
}
int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign)
return (xsign > ysign) ? 1 : -1;
if (xsign == 0)
return 0;
int cmp = compareMagnitude(val);
return (xsign > 0) ? cmp : -cmp;
}
可以看到,分了2种情况,一种是含有小数位相同,另一种时不相同的情况。所以不管2个数的小数位是否相同,都会进行值的比较。
总结
(1)商业计算使用BigDecimal。构造函数一定要用new BigDecimal(String str)方法。
(2)使用参数类型为String的构造函数或valueOf()
方法。但不要传float类型的值,如果不确定,则用new BigDecimal(String str)方法。
(3) BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4) 比较使用compareTo而不是equals。除非你如果需要比较小数位都位数
最后
以上就是幸福白开水为你收集整理的不要以为你用了BigDecimal后,计算结果就一定精确了欢迎关注本人公众号异常再现float和double精度问题BigDecimal分析BigDecimal比较大小注意事项(equals)总结的全部内容,希望文章能够帮你解决不要以为你用了BigDecimal后,计算结果就一定精确了欢迎关注本人公众号异常再现float和double精度问题BigDecimal分析BigDecimal比较大小注意事项(equals)总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复