BigDecimal使用详解
1. 精准计算遇到的问题
整数永远可以用二进制精确表示,小数的二进制表示有时是不可能精确的。
十进制小数如何转化为二进制数,算法是乘以2直,取整数部分直到没有了小数为止。举个例子,十进制小数0.9表示成二进制数:
0.9*2=1.8 取整数部分 1
0.8(1.8的小数部分)*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
......... 0.9二进制表示为(从上往下): 1110011001100......
注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去.
-
程序中小数的计算实例
@Test
public void problem(){
System.out.println(6.3 + 0.1);
System.out.println(6.2 - 0.1);
System.out.println(6.2 * 0.1);
System.out.println(6.3 / 0.1);
}
控制台:
6.3999999999999995
6.1000000000000005
0.6200000000000001
62.99999999999999
试想一下,你的支付宝转出1毛钱,结果变成这么长一串小数是什么感受...
因此小数的二进制表示有时是不可能精确的,他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。float和double只能用来做科学计算或者是工程计算。
在一些要求精确结果的商业计算场合,就不能使用float或者double了,可以使用BigDecimal做为解决方案。
2. BigDecimal介绍
BigDecimal是Java在java.math包中提供的API类,用来对超过16位有效位的数进行精确的运算。
BigDecimal所创建的是对象,因此不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。
2.1. 构造方法
-
public BigDecimal(double val) 将double表示形式转换为BigDecimal (不建议使用)
-
public BigDecimal(int val) 将int表示形式转换成BigDecimal
-
public BigDecimal(String val) 将String表示形式转换成BigDecimal(推荐使用)
-
上代码演示
@Test
public void demo(){
BigDecimal doubleDecimal = new BigDecimal(6.3);
BigDecimal intDecimal = new BigDecimal(6);
BigDecimal stringDecimal = new BigDecimal("6.3");
System.out.println("doubleDecimal=" + doubleDecimal);
System.out.println("intDecimal=" + intDecimal);
System.out.println("stringDecimal=" + stringDecimal);
}
控制台:
doubleDecimal=6.29999999999999982236431605997495353221893310546875
intDecimal=6
stringDecimal=6.3
至于原因,在BigDecimal的构造方法的注释上写的很清楚
/**
* 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
* <tt>(10<sup>scale</sup> × val)</tt> 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
* <i>in</i> 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 <i>exactly</i> equal to
* 0.1, as one would expect. Therefore, it is generally
* recommended that the {@linkplain #BigDecimal(String)
* <tt>String</tt> 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);
}
注释的意思如下:
-
参数类型为double的构造函数的结果可能有些不可预测。
可能大家认为在Java中newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1,但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double。这样,传入到构造方法的值不完全等于 0.1
-
另一方面,String 构造方法是完全可预测的:
newBigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。
-
-
当
double
必须用作BigDecimal
的来源时,请使用`Double.toString(double),转成String,然后使用String构造方法,或使用BigDecimal的静态方法valueOf方法,示例如下:
@Test
public void cons(){
//使用BigDecimal.valueOf()方法将小数转为BigDecimal对象
BigDecimal doubleDecimal = BigDecimal.valueOf(6.3);
//使用Double.toString()方法将小数转为string对象,创建BigDecimal对象
BigDecimal dou2strDecimal = new BigDecimal(Double.toString(6.3));
System.out.println("doubleDecimal=" + doubleDecimal);
System.out.println("dou2strDecimal=" + dou2strDecimal);
//不推荐使用double类型的构造方法
BigDecimal decimal = new BigDecimal(6.111);
System.out.println(decimal);
}
控制台:
doubleDecimal=6.3
dou2strDecimal=6.3
6.11099999999999976552089719916693866252899169921875
2.2. 常用方法
方法 | 描述 |
---|---|
add(BigDecimal) | 对象中的值相加,然后返回这个BigDecimal对象。 |
subtract(BigDecimal) | 对象中的值相减,然后返回这BigDecimal个对象。 |
multiply(BigDecimal) | 对象中的值相乘,然后返回这BigDecimal个对象。 |
divide(BigDecimal) | 对象中的值相除,然后返回这个BigDecimal对象。 |
toString() | 将BigDecimal对象的数值转换成字符串。 |
doubleValue() | 将BigDecimal对象中的值以双精度数返回。 |
floatValue() | 将BigDecimal对象中的值以单精度数返回。 |
longValue() | 将BigDecimal对象中的值以长整数返回。 |
intValue() | 将BigDecimal对象中的值以整数返回。 |
3. BigDecimal数学运算
3.1. 基本运算
BigDecimal的加减乘除用法很简单,直接上代码
@Test
public void math(){
BigDecimal a = new BigDecimal("6.5");
BigDecimal b = new BigDecimal("1.3");
System.out.println("a + b =" + a.add(b));
System.out.println("a - b =" + a.subtract(b));
System.out.println("a * b =" + a.multiply(b));
System.out.println("a / b =" + a.divide(b));
}
控制台:
a + b =7.8
a - b =5.2
a * b =8.45
a / b =5
3.2. 舍入模式
这里有一点需要注意的是除法运算divide.
BigDecimal除法可能出现不能整除的情况,会报错,比如 6.6 / 1.3,这时会报错
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
-
除法除不尽解决方法:divide方法有可以传三个参数
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
第一参数表示除数
第二个参数表示小数点后保留位数
第三个参数表示舍入模式。
只有在作除法运算或四舍五入时才用到舍入模式,有下面这几种:
-
ROUND_UP :向远离零的方向舍入。舍弃非零部分,并将非零舍弃部分相邻的一位数字+1。
-
ROUND_DOWN :向接近零的方向舍入。舍弃非零部分,同时不会非零舍弃部分相邻的一位数字加一,采取截取行为。
-
ROUND_CEILING :向正无穷的方向舍入。如果为正数,舍入结果同ROUND_UP一致;如果为负数,舍入结果同ROUND_DOWN一致。注意:此模式不会减少数值大小。
-
ROUND_FLOOR :向负无穷的方向舍入。如果为正数,舍入结果同ROUND_DOWN一致;如果为负数,舍入结果同ROUND_UP一致。注意:此模式不会增加数值大小。
-
ROUND_HALF_UP :“四舍五入”,向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分>= 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。
-
ROUND_HALF_DOWN :“五舍六入”,向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向下舍入的舍入模式。如果舍弃部分> 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。
-
ROUND_HALF_EVEN :向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则相邻的偶数舍入。如果舍弃部分左边的数字奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。注意:在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况,如果前一位为奇数,则入位,否则舍去。
-
ROUND_UNNECESSARY :断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。
按照各自的需要,可传入合适的第三个参数。四舍五入采用 ROUND_HALF_UP
@Test
public void divide(){
BigDecimal a = new BigDecimal("6.6");
BigDecimal b = new BigDecimal("1.3");
//计算结果四舍五入,保留2位小数
BigDecimal val = a.divide(b, 2, RoundingMode.HALF_EVEN);
System.out.println("a / b = " + val);
}
-
减乘除其实最终都返回的是一个新的BigDecimal对象,因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象
@Test
public void newBic(){
BigDecimal a = new BigDecimal("6.6");
BigDecimal b = new BigDecimal("1.3");
a.add(b);
//输出6.6. 加减乘除方法会返回一个新的BigDecimal对象,原来的a不变
System.out.println(a);
}
3.3. 比较大小
@Test
public void newBic(){
BigDecimal a = new BigDecimal ("100.01");
BigDecimal b = new BigDecimal ("200.02");
//使用compareTo方法比较
//compareTo方法返回值:-1, 0, 1分别表示小于 等于 大于
//注意:a、b均不能为null,否则会报空指针
if(a.compareTo(b) == -1){
System.out.println("a小于b");
}
if(a.compareTo(b) == 0){
System.out.println("a等于b");
}
if(a.compareTo(b) == 1){
System.out.println("a大于b");
}
}
3.4. BigDecimal转String
@Test
public void toStr(){
// 普通的数字字符串
System.out.println(new BigDecimal("100.00").toString());
// 去除末尾多余的0,小数位都是0会输出科学计数法
System.out.println(new BigDecimal("100.00")
.stripTrailingZeros().toString());
// toPlainString()避免输出科学计数法
System.out.println(new BigDecimal("100.00")
.stripTrailingZeros().toPlainString());
}
4.总结
-
商业计算使用BigDecimal。
-
尽量使用参数类型为String的构造函数。
-
BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。