Java double float和BigDecimal以及货币计算方式

在文章之前先看一个简单的例子:

public static void main(String[] args) {
	System.out.println(0.7-0.4);
	System.out.println(0.05 + 0.01);
	System.out.println(4.012*100);
	System.out.println(new BigDecimal(3.05));
	System.out.println(new BigDecimal(0.1));
}

看似简单的数值计算输出结果会令不少初入的程序员咂舌,输出结果如下:

0.29999999999999993
0.060000000000000005
401.19999999999993
3.04999999999999982236431605997495353221893310546875
0.1000000000000000055511151231257827021181583404541015625

不仅仅是Java,其它很多编程语言也有这样的问题。多数情况下的计算结果是正确的,但是上面这种情况在计算机中也不是错误的。因为float或者double在计算机中只是为了提供较为精确的数值而设计的,如果是int或者long不会出现这种问题。在实际应用中int或者long不会出现小数点,很多应用中要求数值保留两位小数点,像一些金钱计算或者工程测量中,这种情况下又得借用float或者double,既然精度表示上面有误差,只要我们使用四舍五入就可以了,Math中有一个方法round,但是不能保留小数点,因为它的返回值只有int和long两种类型,是不是下面设计一下就可以了呢?

public double round(double value){
    return Math.round(value*100)/100.0;
}

但是这种方式也不行,上面有代码中已经包含了这样一中情况:4.012*100=401.19999999999993。

如果我们要进行float或者double类型的简单数值进行精确的四舍五入,几乎不能做任何运算。 即使使用java.text.DecimalFormat正确处理了四舍五入,但是在一些特殊情况下也无法满足功能需求。下面一个示例改自《Effective Java》,假设你的口袋内有¥1.0,货架上面有一排美味的糖果,标价分别为¥0.1、¥0.2、¥0.3...一直到¥1.0。你打算从¥0.1开始买,每种买一颗,一直到不能买为止,那么你可以买多少颗糖果?还能找回多少零钱?下面是一个简单的程序,用来解决这个问题。

private static void test(){
	double money=1.00;
	int buy=0;
	for(double price=0.1;price<money;price+=0.1){
		money-=price;
		buy++;
	}
	System.out.println(money);//0.3999999999999999
	System.out.println(buy);//3
}

运行之后发现只可以买到3颗糖,但是实际上可以买4颗刚刚好,计算机计算的结果显然是不正确的。

在《Effective Java》这本书中也提到一个原则:如果需要精确的答案,请避免使用float和double。

float和double类型主要是为了科学计算工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,他们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1是不可能的。

BigDecimal

如何解决上述购买糖的问题,一种是计算过程更改货币单位转换为int或者long进行计算,还有一种方式是使用BigDecimal类,BigDecimal与基本运算类型相比很不方便,而且很慢。但是对于解决这种简单问题,这种缺点是可以接受的,因为基本运算会更让人难以忍受。BigDecimal提供了加减乘除四则运算相应的方法:

public BigDecimal add(BigDecimal value);         //加法
public BigDecimal subtract(BigDecimal value);    //减法 
public BigDecimal multiply(BigDecimal value);    //乘法
public BigDecimal divide(BigDecimal value);      //除法

如果使用BigDecimal还有另外一个好处,它可以完全控制舍入,当设计到舍入的时候,它提供了8中舍入模式,比较常用的是四舍五入模式。舍入模式是通过RoundingMode枚举类型进行控制的,如果未指定舍入模式,并且无法表示准确结果,则抛出一个异常,如果计算结果是一个无限小数一定要设定舍入模式。不但可以控制舍入,还可以控制小数点的位数,在使用的时候已经为我们提供了足够的条件满足各种不同的业务需求。

在使用BigDecimal的时候,尽量使用String来构造BigDecimal,文章开始部分已经有相应的示例演示,我们看一下JDK中的描述:

1.此构造方法的结果有一定的不可预知性。有人可能认为在 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 的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用 Double.toString(double) 方法,然后使用 BigDecimal(String) 构造方法,将 double 转换为 String。要获取该结果,请使用 static valueOf(double) 方法。

根据第3条知道,如果我们必须使用double来作为一个数据源传入的话,可以使用BigDecimal静态方法valueOf(double),该方法会返回一个正确的值。

BigDecimal做四则运算比较麻烦,下面是一个简单的计算加减乘除工具类:

public class DecimalUtil {

	/**
	 * 加法
	 * 
	 * @param v1
	 * @param v2
	 * @return String
	 */
	public static String add(String v1, String v2) {
		BigDecimal b1 = new BigDecimal(v1);
		BigDecimal b2 = new BigDecimal(v2);
		return b1.add(b2).toString();
	}

	/**
	 * 减法
	 * 
	 * @param v1
	 * @param v2
	 * @return
	 */
	public static String subtract(String v1, String v2) {
		BigDecimal b1 = new BigDecimal(v1);
		BigDecimal b2 = new BigDecimal(v2);

		return b1.subtract(b2).toString();
	}

	/**
	 * 乘法
	 * 
	 * @param v1
	 * @param v2
	 * @return
	 */
	public static String multiply(String v1, String v2) {
		BigDecimal b1 = new BigDecimal(v1);
		BigDecimal b2 = new BigDecimal(v2);

		return b1.multiply(b2).toString();
	}

	/**
	 * 除法
	 * 
	 * @param v1
	 * @param v2
	 * @return
	 */
	public static String divide(String v1, String v2) {
		return divide(v1, v2, 2, RoundingMode.HALF_UP);
	}

	/**
	 * 除法
	 * 
	 * @param v1
	 * @param v2
	 * @param scale
	 * @param mode
	 * @return
	 */
	public static String divide(String v1, String v2, int scale, RoundingMode mode) {
		BigDecimal b1 = new BigDecimal(v1);
		BigDecimal b2 = new BigDecimal(v2);
		return b1.divide(b2, scale, mode).toString();
	}
}

对于浮点型数据,不要再程序中试图比较两个浮点数是否相等,虽然语法上面允许这样做,可以用两个浮点数差的绝对值是否小于一个足够小的值来判断

double a=0.4;
double b=0.7-0.3;
System.out.println(b);

//错误的做法
if(a==b){
	System.out.println("a==b");
}else{
	System.out.println("a!=b");
}

//正确的做法
if(Math.abs(a-b)<1e-6){
	System.out.println("a==b");
}else{
	System.out.println("a!=b");
}

参考资料

java中浮点数的比较(double, float)

恶心的0.5四舍五入问题

ava中关于 BigDecimal 的一个导致double精度损失的"bug"

评论

您确定要删除吗?删除之后不可恢复