欢迎关注本人公众号
异常再现
大家应该都已经知道,涉及到金钱的计算应该使用BigDecimal,没有使用BigDecimal的已经被开除。
但是使用了BigDecimal后计算结果就一定是精确的吗?未必。
看下面测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17BigDecimal 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));
输出结果为:
1
2
3
4
53.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)
这个方法是无法保证精度的,源码注释中也已经写明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/** * 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)
:
1
2
3
4public BigDecimal(String val) { this(val.toCharArray(), 0, val.length()); }
当然使用BigDecimal valueOf(double val)
也是可以的,valueOf实际是先将double或flout的数据转为了string:
1
2
3
4
5
6
7
8public 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)); }
另外,特别注意,以下代码是错误的哦:
1
2
3
4
5
6
7
8
9
10
11@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); }
输出:
1
2
3
40.1 0.10000000149011612 0.10000000149011612
其实这个不是BigDecimal.valueOf的问题,而是float转double时精度丢失了。
BigDecimal比较大小注意事项(equals)
在项目中使用BigDecimal的equals方法比较大小时,结果不为true,直接上示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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); }
结果:
1
2
3a equals b -->true c equals d -->false
可以看到a和b比较结果是true,c和d比较的结果为fasle
c、d使用传入字符串的构造器(等同于数据库查询出来的值)
项目中从数据库查询出来的值进行比较时(上例中c、d)显然不是我们期望的结果,因此修改为如下方法
1
2
3boolean result2 = c.compareTo(d) == 0; System.out.println("c compareTo d -->" + result2);
结果:
c compareTo d -->true
我们来看下构造器:
1
2
3
4public BigDecimal(String val) { this(val.toCharArray(), 0, val.length()); }
该构造器的注释中有这么一段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23* <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方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@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方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/** * 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后内容请搜索靠谱客的其他文章。
发表评论 取消回复