1.引言
借用《Effactive Java》这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。但是,商业计算往往要求结果精确,这时候BigDecimal就派上大用场啦。
2.BigDecimal简介
BigDecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。因此,BigDecimal表示的数值是(unscaledValue × 10-scale)。
3.测试代码
3.1构造函数(主要测试参数类型为double和String的两个常用构造函数)
BigDecimal aDouble =newBigDecimal(1.22);
System.out.println("construct witha double value: " + aDouble);
BigDecimal aString =newBigDecimal("1.22");
System.out.println("constructwith a String value: " + aString);
你认为输出结果会是什么呢?如果你没有认为第一个会输出1.22,那么恭喜你答对了,输出结果如下:
construct with adoublevalue:1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22
JDK的描述:1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
2、另一方面,String 构造方法是完全可预知的:写入newBigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。
3、当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。要获取该结果,请使用static valueOf(double)方法。
3.2 加法操作
BigDecimal a =newBigDecimal("1.22");
System.out.println("construct witha String value: " + a);
BigDecimal b =newBigDecimal("2.22");
a.add(b);
System.out.println("aplus b is :" + a);
我们很容易会认为会输出:
construct with a Stringvalue: 1.22
a plus b is :3.44
但实际上a plus b is : 1.22
4.源码分析
4.1 valueOf(doubleval)方法
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns'0.0', so we cannotfastpath
// to use the constant ZERO. This mightbe important enough to
// justify a factory approach, a cache,or a few private
// constants, later.
returnnewBigDecimal(Double.toString(val));//见3.1关于JDK描述的第三点
}
4.2 add(BigDecimal augend)方法
public BigDecimal add(BigDecimal augend) {
long xs =this.intCompact; //整型数字表示的BigDecimal,例a的intCompact值为122
long ys = augend.intCompact;//同上
BigInteger fst = (this.intCompact!=INFLATED) ?null :this.intVal;//初始化BigInteger的值,intVal为BigDecimal的一个BigInteger类型的属性
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)//判断有无溢出
returnBigDecimal.valueOf(sum,rscale);//返回使用BigDecimal的静态工厂方法得到的BigDecimal实例
}
if (fst ==null)
fst=BigInteger.valueOf(xs);//BigInteger的静态工厂方法
if (snd ==null)
snd =BigInteger.valueOf(ys);
BigInteger sum =fst.add(snd);
return (fst.signum == snd.signum)?new BigDecimal(sum,INFLATED, rscale, 0) :
newBigDecimal(sum,compactValFor(sum),rscale, 0);//返回通过其他构造方法得到的BigDecimal对象
}
以上只是对加法源码的分析,减乘除其实最终都返回的是一个新的BigDecimal对象,因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b);虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b);
5.java的四舍五入详解
四舍五入是我们小学的数学问题,这个问题对于我们程序猿来说就类似于1到10的加减乘除那么简单了。在讲解之间我们先看如下一个经典的案例:
[java] view plaincopy
public static void main(String[] args){
System.out.println("12.5的四舍五入值:" + Math.round(12.5));
System.out.println("-12.5的四舍五入值:" + Math.round(-12.5));
}
Output:
12.5的四舍五入值:13
-12.5的四舍五入值:-12
这是四舍五入的经典案例,也是我们参加校招时候经常会遇到的(貌似我参加笔试的时候遇到过好多次)。从这儿结果中我们发现这两个绝对值相同的数字,为何近似值会不同呢?其实这与Math.round采用的四舍五入规则来决定。
四舍五入其实在金融方面运用的非常多,尤其是银行的利息。我们都知道银行的盈利渠道主要是利息差,它从储户手里收集资金,然后放贷出去,期间产生的利息差就是银行所获得的利润。如果我们采用平常四舍五入的规则话,这里采用每10笔存款利息计算作为模型,如下:
四舍:0.000、0.001、0.002、0.003、0.004。这些舍的都是银行赚的钱。
五入:0.005、0.006、0.007、0.008、0.009。这些入的都是银行亏的钱,分别为:0.005、0.004、.003、0.002、0.001。
所以对于银行来说它的盈利应该是0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 -0.002 - 0.001 = -0.005。从结果中可以看出每10笔的利息银行可能就会损失0.005元,千万别小看这个数字,这对于银行来说就是一笔非常大的损失。面对这个问题就产生了如下的银行家涉入法了。该算法是由美国银行家提出了,主要用于修正采用上面四舍五入规则而产生的误差。如下:
舍去位的数值小于5时,直接舍去。
舍去位的数值大于5时,进位后舍去。
当舍去位的数值等于5时,若5后面还有其他非0数值,则进位后舍去,若5后面是0时,则根据5前一位数的奇偶性来判断,奇数进位,偶数舍去。
对于上面的规则我们举例说明
11.556 = 11.56 ------六入
11.554 = 11.55 -----四舍
11.5551 = 11.56 -----五后有数进位
11.545 = 11.54 -----五后无数,若前位为偶数应舍去
11.555 = 11.56 -----五后无数,若前位为奇数应进位
下面实例是使用银行家舍入法:
[java] view plaincopy
public static void main(String[] args){
BigDecimal d = newBigDecimal(100000); //存款
BigDecimal r = newBigDecimal(0.001875*3); //利息
BigDecimal i =d.multiply(r).setScale(2,RoundingMode.HALF_EVEN); //使用银行家算法
System.out.println("季利息是:"+i);
}
Output:
季利息是:562.50
在上面简单地介绍了银行家舍入法,目前java支持7中舍入法:
1、ROUND_UP:远离零方向舍入。向绝对值最大的方向舍入,只要舍弃位非0即进位。
2、ROUND_DOWN:趋向零方向舍入。向绝对值最小的方向输入,所有的位都要舍弃,不存在进位情况。
3、ROUND_CEILING:向正无穷方向舍入。向正最大方向靠拢。若是正数,舍入行为类似于ROUND_UP,若为负数,舍入行为类似于ROUND_DOWN。Math.round()方法就是使用的此模式。
4、ROUND_FLOOR:向负无穷方向舍入。向负无穷方向靠拢。若是正数,舍入行为类似于ROUND_DOWN;若为负数,舍入行为类似于ROUND_UP。
5、HALF_UP:最近数字舍入(5进)。这是我们最经典的四舍五入。
6、HALF_DOWN:最近数字舍入(5舍)。在这里5是要舍弃的。
7、HAIF_EVEN:银行家舍入法。
提到四舍五入那么保留位就必不可少了,在java运算中我们可以使用多种方式来实现保留位。
保留位
方法一:四舍五入
[java] view plaincopy
double f = 111231.5585;
BigDecimal b = new BigDecimal(f);
double f1 = b.setScale(2, RoundingMode.HALF_UP).doubleValue();
在这里使用BigDecimal ,并且采用setScale方法来设置精确度,同时使用RoundingMode.HALF_UP表示使用最近数字舍入法则来近似计算。在这里我们可以看出BigDecimal和四舍五入是绝妙的搭配。
方式二:
[java] view plaincopy
java.text.DecimalFormat df =new java.text.DecimalFormat(”#.00″);
df.format(你要格式化的数字);
例:new java.text.DecimalFormat(”#.00″).format(3.1415926)
#.00 表示两位小数 #.0000四位小数 以此类推…
方式三:
[java] view plaincopy
double d = 3.1415926;
String result = String.format(”%.2f”);
%.2f %. 表示 小数点前任意位数 2 表示两位小数 格式后的结果为f 表示浮点型。
方式四:
此外如果使用struts标签做输出的话,有个format属性,设置为format="0.00"就是保留两位小数
例如:
[java] view plaincopy
<bean:write name="entity"property="dkhAFSumPl" format="0.00" />
或者
<fmt:formatNumbertype="number" value="${10000.22/100}"maxFractionDigits="0"/>
maxFractionDigits表示保留的位数
6.总结
(1)商业计算使用BigDecimal。
(2)尽量使用参数类型为String的构造函数。
(3) BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4)我们往往容易忽略JDK底层的一些实现细节,导致出现错误,需要多加注意。
7.封装Arith类
[java] view plain copy
package lj.basic;
import java.math.BigDecimal;
public class Arith
{
private static final int DEF_DIV_SCALE= 10;
private Arith()
{
}
/**
*
*
*
* 提供精确的加法运算。
*
*
*
* @param v1
* 被加数
*
*
*
* @param v2
* 加数
*
*
*
* @return 两个参数的和
*/
public static double add(double v1,double v2)
{
BigDecimal b1 = newBigDecimal(Double.toString(v1));
BigDecimal b2 = newBigDecimal(Double.toString(v2));
returnb1.add(b2).doubleValue();
}
/**
*
*
*
* 提供精确的减法运算。
*
*
*
* @param v1
* 被减数
*
*
*
* @param v2
* 减数
*
*
*
* @return 两个参数的差
*/
public static double sub(double v1,double v2)
{
BigDecimal b1 = newBigDecimal(Double.toString(v1));
BigDecimal b2 = newBigDecimal(Double.toString(v2));
returnb1.subtract(b2).doubleValue();
}
/**
*
*
*
* 提供精确的乘法运算。
*
*
*
* @param v1
* 被乘数
*
*
*
* @param v2
* 乘数
*
*
*
* @return 两个参数的积
*/
public static double mul(double v1,double v2)
{
BigDecimal b1 = newBigDecimal(Double.toString(v1));
BigDecimal b2 = newBigDecimal(Double.toString(v2));
returnb1.multiply(b2).doubleValue();
}
/**
*
*
*
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
*
*
*
* 小数点以后10位,以后的数字四舍五入。
*
*
*
* @param v1
* 被除数
*
*
*
* @param v2
* 除数
*
*
*
* @return 两个参数的商
*/
public static double div(double v1,double v2)
{
return div(v1, v2,DEF_DIV_SCALE);
}
/**
*
*
*
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
*
*
*
* 定精度,以后的数字四舍五入。
*
*
*
* @param v1
* 被除数
*
*
*
* @param v2
* 除数
*
*
*
* @param scale
* 表示表示需要精确到小数点以后几位。
*
*
*
* @return 两个参数的商
*/
public static double div(double v1,double v2, int scale)
{
if (scale < 0)
{
throw newIllegalArgumentException(
"The scale must bea positive integer or zero");
}
BigDecimal b1 = newBigDecimal(Double.toString(v1));
BigDecimal b2 = newBigDecimal(Double.toString(v2));
return b1.divide(b2, scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
*
*
*
* 提供精确的小数位四舍五入处理。
*
*
*
* @param v
* 需要四舍五入的数字
*
*
*
* @param scale
* 小数点后保留几位
*
*
*
* @return 四舍五入后的结果
*/
public static double round(double v,int scale) {
if (scale < 0)
{
throw newIllegalArgumentException(
"The scale must bea positive integer or zero");
}
BigDecimal b = newBigDecimal(Double.toString(v));
BigDecimal one = newBigDecimal("1");
return b.divide(one, scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
文章出自http://blog.csdn.net/liujian928730/article/details/50542534