使⽤BigDecimal进⾏精确运算以及格式化输出数字
⼀、引⾔
借⽤《Effactive Java》这本书中的话,float和double类型的主要设计⽬标是为了科学计算和⼯程计算。他们执⾏⼆进制浮点运算,这是为了在⼴域数值范围上提供较为精确的快速近似计算⽽精⼼设计的。然⽽,它们没有提供完全精确的结果,所以不应该被⽤于要求精确结果的场合。但是,货币计算往往要求结果精确,这时候可以使⽤int、long或BigDecimal。本⽂主要讲述BigDecimal使⽤过程中的⼀些陷阱、建议和技巧。
⼆、不可变性
BigDecimal是不可变类,每⼀个操作(加减乘除等)都会返回⼀个新的对象, 下⾯以加法操作为例。
BigDecimal a =new BigDecimal("1.22");
System.out.println("construct with a String value: " + a);
BigDecimal b =new BigDecimal("2.22");
a.add(b);
System.out.println("a plus b is : " + a);
我们很容易会认为会输出:
construct with a String value: 1.22
a plus
b is :3.44
但实际上a plus b is : 1.22
下⾯我们就来分析⼀下加法操作的源码
public BigDecimal add(BigDecimal augend) {
long xs =this.intCompact; //整型数字表⽰的BigDecimal,例a的intCompact值为122
long ys = augend.intCompact;//同上
//初始化BigInteger的值,intVal为BigDecimal的⼀个BigInteger类型的属性
BigInteger fst = (this.intCompact !=INFLATED) ?null :this.intVal;
BigInteger snd =(augend.intCompact !=INFLATED) ?null : augend.intVal;
int rscale =this.scale;//⼩数位数
long sdiff = (long)rscale - augend.scale;//⼩数位数之差
if (sdiff != 0) {//取⼩数位数多的为结果的⼩数位数
if (sdiff < 0) {
int raise =checkScale(-sdiff);
rscale =augend.scale;
if (xs ==INFLATED ||(xs = longMultiplyPowerTen(xs,raise)) ==INFLATED)
fst =bigMultiplyPowerTen(raise);
}else {
int raise =augend.checkScale(sdiff);
if (ys ==INFLATED ||(ys =longMultiplyPowerTen(ys,raise)) ==INFLATED)
snd = augend.bigMultiplyPowerTen(raise);
}
}
if (xs !=INFLATED && ys !=INFLATED) {
long sum = xs + ys;
if ( (((sum ^ xs) &(sum ^ ys))) >= 0L)//判断有⽆溢出
//返回使⽤BigDecimal的静态⼯⼚⽅法得到的BigDecimal实例
return BigDecimal.valueOf(sum,rscale);
}
if (fst ==null)
fst =BigInteger.valueOf(xs);//BigInteger的静态⼯⼚⽅法
if (snd ==null)
snd =BigInteger.valueOf(ys);
BigInteger sum =fst.add(snd);
//返回通过其他构造⽅法得到的BigDecimal对象
return (fst.signum == snd.signum) ?new BigDecimal(sum,INFLATED, rscale, 0) :
new BigDecimal(sum,compactValFor(sum),rscale, 0);
}
因为BigInteger与BigDecimal都是不可变的(immutable)的,在进⾏每⼀步运算时,都会产⽣⼀个新的对象,所以 a.add(b)虽然做了加法操作,但是a并没有保存加操作后的值,正确的⽤法应该是a=a.add(b); 减乘除操作也是⼀样的返回⼀个新的BigDecimal对象。
三、构造函数和valueOf⽅法
⾸先看如下⼀段代码:
// use constructor BigDecimal(double)
BigDecimal aDouble =new BigDecimal(1.22);
System.out.println("construct with a double value: " + aDouble);
// use constructor BigDecimal(String)
BigDecimal aString = new BigDecimal("1.22");
System.out.println("construct with a String value: " + aString);
// use constructor BigDecimal.valueOf(double)
BigDecimal aValue = BigDecimal.valueOf(1.22);
System.out.println("use valueOf method: " + aValue);
你认为输出结果会是什么呢?如果你认为第⼀个会输出1.22,那么恭喜你答错了,输出结果如下:
construct with a double value: 1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22
use valueOf method: 1.22
为什么会这样呢?JavaDoc对于BigDecimal(double)有很详细的说明:
1、参数类型为double的构造⽅法的结果有⼀定的不可预知性。有⼈可能认为在Java中new BigDecimal(0.1)所创建的BigDecimal的值正好等于 0.1(⾮标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1⽆法准确地表⽰为 double(或者说对于该情况,不能表⽰为任何有限长度的⼆进制⼩数)。这样,传⼊到构造⽅法的值不会正好等于0.1(虽然表⾯上等于该值)。
2、另⼀⽅⾯,String 构造⽅法是完全可预知的:new BigDecimal("0.1") 将创建⼀个 BigDecimal,它的值正好等于期望的0.1。因此,⽐较⽽⾔,通常建议优先使⽤String构造⽅法。
3、当 double 必须⽤作BigDecimal的来源时,请注意,此构造⽅法提供了⼀个精确转换;它不提供与
以下操作相同的结果:先使⽤String(double)⽅法将double转换为String,然后使⽤BigDecimal(String)构造⽅法。要获取该结果,使⽤static valueOf(double)⽅法。
BigDecimal.valueOf(double) 使⽤由 String(double)⽅法提供的 double的标准化字符串表⽰形式( canonical string representation)将 double 转换成 BigDecimal 。这也是⽐较推荐的⼀种⽅式。
BigDecimal.valueOf(double)还有⼀个重载的⽅法 BigDecimal.valueOf(long),对于某些常⽤值(0到10) BigDecimal在内部做了缓存,如果传递的参数值范围为[0, 10], 这个⽅法直接返回缓存中相应的BigDecimal对象。
java源码如下:
/**
* Translates a {@code long} value into a {@code BigDecimal}
* with a scale of zero. This {@literal "static factory method"}
* is provided in preference to a ({@code long}) constructor
* because it allows for reuse of frequently used
* {@code BigDecimal} values.
*
* @param val value of the {@code BigDecimal}.
* @return a {@code BigDecimal} whose value is {@code val}.
*/
public static BigDecimal valueOf(long val) {
if (val >= 0 && val < zeroThroughTen.length)
return zeroThroughTen[(int)val];
else if (val != INFLATED)
return new BigDecimal(null, val, 0, 0);
return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
}
// Cache of common small BigDecimal values.
private static final BigDecimal zeroThroughTen[] = {
new BigDecimal(BigInteger.ZERO, 0, 0, 1),
new BigDecimal(BigInteger.ONE, 1, 0, 1),
new BigDecimal(BigInteger.valueOf(2), 2, 0, 1),
new BigDecimal(BigInteger.valueOf(3), 3, 0, 1),
new BigDecimal(BigInteger.valueOf(4), 4, 0, 1),
new BigDecimal(BigInteger.valueOf(5), 5, 0, 1),
new BigDecimal(BigInteger.valueOf(6), 6, 0, 1),
new BigDecimal(BigInteger.valueOf(7), 7, 0, 1),
new BigDecimal(BigInteger.valueOf(8), 8, 0, 1),
new BigDecimal(BigInteger.valueOf(9), 9, 0, 1),
new BigDecimal(BigInteger.TEN, 10, 0, 2),
};
附上相应的测试代码:
BigDecimal a1 = BigDecimal.valueOf(10);
BigDecimal a2 = BigDecimal.valueOf(10);
System.out.println(a1 == a2); // true
BigDecimal a3 = BigDecimal.valueOf(11);
BigDecimal a4 = BigDecimal.valueOf(11);
System.out.println(a3 == a4); // false
四、equals⽅法
BigDecimal.equals⽅法是有问题的.仅当你确定⽐较的值有着相同的标度时才可使⽤. 因此,当你校验相等性时注意 - BigDecimal有⼀个标度,⽤于相等性⽐较. ⽽compareTo⽅法则会忽略这个标度(scale).
BigDecimal的equals⽅法源码如下:
@Override
public boolean equals(Object x) {
// 必须是BigDecimal实例
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());
}
参见以下测试代码:
// 打印false
System.out.println(new BigDecimal("0.0").equals(new BigDecimal("0.00")));
// 打印false
System.out.println(new BigDecimal("0.0").hashCode() == (new BigDecimal("0.00")).hashCode());
// 打印0
System.out.println(new BigDecimal("0.0")pareTo(new BigDecimal("0.00")));
五、对除法使⽤标度
BigDecimal对象的精度没有限制。如果结果不能终⽌,divide⽅法将会抛出ArithmeticException, 如1 / 3 = 。所以强烈推荐使⽤重载⽅法divide(BigDecimal d, int scale, int roundMode)指定标度和舍⼊模式来避免以上异常。
参见以下测试代码:
//java.lang.ArithmeticException: Non-terminating decimal expansion;
//no exact representable decimal result.
try {
BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3));
} catch (ArithmeticException ex) {
System.out.Message());
}
// always use a scale and the rounding mode of your choice
// 0.33
System.out.println(BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3), 2, BigDecimal.ROUND_HALF_UP));
六、总结
(1)商业计算使⽤BigDecimal。
(2)使⽤参数类型为String的构造函数,将double转换成BigDecimal时⽤BigDecimal.valueOf(double),做除法运算时使⽤重载的⽅法divide(BigDecimal d, int scale, int roundMode)。
(3)BigDecimal是不可变的(immutable)的,在进⾏每⼀步运算时,都会产⽣⼀个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4)尽量使⽤compareTo⽅法⽐较两个BigDecimal对象的⼤⼩。
再来看看数字格式化输出的概念:
有时候我需要将数字按照本地的风格习惯进⾏数字的显⽰,可以⽤NumberFormat,
此类的定义如下:
public abstract class NumberFormat extends Format
MessageFormat 、DateFormat 、NumberFormat 是 Format 三个常⽤的⼦类,如果要想进⼀步完成⼀个好的国际化程序,则肯定需要同时使⽤这样三个类完成,根据不同的国家显⽰贷币的形式。
此类还是在 包中,所以直接导⼊此包即可。
* ;
public class NumberFormatDemo01{
public static void main(String args[]){
NumberFormat nf = null ; // 声明⼀个NumberFormat对象
nf = Instance() ; // 得到默认的数字格式化显⽰
System.out.println("格式化之后的数字:" + nf.format(10000000)) ;
System.out.println("格式化之后的数字:" + nf.format(1000.345)) ;
}
};
在美国,"."是⼩数点,但在其它地⽅就不⼀定了。如何处理这个呢? 包中的⼀些包可以处理这类问题。下⾯的简单范例使⽤那些类解决上⾯提出的问题:
NumberFormat;
import java.util.Locale;
public class DecimalFormat1 {
public static void main(String args[]) {
// 得到本地的缺省格式
NumberFormat nf1 = Instance();
System.out.println(nf1.format(1234.56));
// 得到德国的格式
NumberFormat nf2 = Instance(Locale.GERMAN);
System.out.println(nf2.format(1234.56));
} }
如果你在美国,运⾏程序后输出: 1,234.56 1.234,56
换句话说,在不同的地⽅使⽤不同的习惯表⽰数字。 Instance() ⽅法返回NumberFormat的⼀个实例(实际上是NumberFormat具体的⼀个⼦类,例如DecimalFormat), 这适合根据本地设置格式化⼀个数字。你也可以使⽤⾮缺省的地区设置,例如德国。然后格式化⽅法根据特定的地区规则格式化数字。这个程序也可以使⽤⼀个简单的形式:
DecimalFormat;
import java.util.Locale;
public class DecimalFormat2 {
public static void main(String args[]) {
// 得到本地的缺省格式
DecimalFormat df1 = new DecimalFormat("####.000");
System.out.println(df1.format(1234.56));
// 得到德国的格式
Locale.setDefault(Locale.GERMAN);
DecimalFormat df2 = new DecimalFormat("####.000");
System.out.println(df2.format(1234.56));
}
}
在这个例⼦中设置了数字的格式,使⽤像"####.000"的符号。这个模式意味着在⼩数点前有四个数字,如果不够就空着,⼩数点后有三位数字,不⾜⽤0 补齐。
程序的输出: 1234.560 1234,560
相似的,也可以控制指数形式的格式,例如:
DecimalFormat;
public class DecimalFormat3 {
public static void main(String args[]) {
DecimalFormat df = new DecimalFormat("0.000E0000"); System.out.println(df.format(1234.56));
}
bigdecimal除法保留小数}
输出: 1.235E0003
对于百分数:
NumberFormat;
public class DecimalFormat4 {
public static void main(String args[]) {
NumberFormat nf = PercentInstance(); System.out.println(nf.format(0.47));
}
}
输出: 47%
⾄此,你已经看到了格式化数字的⼏个不同的技术。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论