啥都不说,先看一个例子
public static void main(String[] args) { //加减乘除都出现了对应的精度问题 System.out.println(0.05 + 0.01); //0.060000000000000005 System.out.println(1.0 - 0.42); //0.5800000000000001 System.out.println(4.015 * 100); //401.49999999999994 System.out.println(123.3 / 100); //1.2329999999999999 }
有没有一种触目惊心的感觉,感觉回去检查检查自己的代码,有没有一些数值运算吧,哈哈。这个问题相当严重,比如你有9.999999999999元,你的计算机是不会认为你可以购买10元的商品的。在有的编程语言中提供了专门的货币类型来处理这种情况,但是Java没有。
下面会解释原因以及提出解决方案。但结论可以先给大家:
Java中的简单浮点数类型float和double不能够进行运算。
问题分析
我们的第一个反应是做四舍五入。Math类中的round方法不能设置保留几位小数,我们只能象这样(保留两位):
public double round(double value){ return Math.round(value*100)/100.0; }
非常不幸,上面的代码并不能正常工作,给这个方法传入4.015它将返回4.01而不是4.02,如我们在上面看到的4.015*100=401.49999999999994
因此如果我们要做到精确的四舍五入,不能利用简单类型做任何运算
java.text.DecimalFormat也不能解决这个问题:
System.out.println(new java.text.DecimalFormat("0.00").format(4.025)); //4.02
现在我们已经可以解决这个问题了,原则是使用BigDecimal并且一定要用String来构造。否则见下面例子
public static void main(String[] args) { double g = 12.35; BigDecimal bigG = new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); System.out.println(bigG.doubleValue()); //期望得到12.4 但实际输出:12.3 }
大概原因:定义double g= 12.35; 而在计算机中二进制表示可能这是样:定义了一个g=12.34444444444444449,
new BigDecimal(g) g还是12.34444444444444449
new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); 得到12.3
正确的定义方式是使用字符串构造函数:
new BigDecimal("12.35").setScale(1, BigDecimal.ROUND_HALF_UP)
解决方案
上面也已经提到过了,我们可以借助BigDecimal来解决这个问题。因此此处我提供一共工具类,**以后大家java中的数值运算都采用此工具类处理,就绝对不会有精度问题了:MathHelper **
import java.math.BigDecimal; /** * 精确的加减乘除的工具类 * * @author fangshixiang * @description // * @date 2019/1/16 16:34 */ public abstract class MathHelper { //默认10才能进位 private static final int DEF_DIV_SCALE = 10; /** * 提供精确的加法运算。 * * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 double */ public static BigDecimal add(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.add(b2); } /** * 提供精确的减法运算。 * * @param v1 被减数 * @param v2 减数 * @return 两个参数的差 double */ public static BigDecimal sub(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.subtract(b2); } /** * 提供精确的乘法运算。 * * @param v1 被乘数 * @param v2 乘数 * @return 两个参数的积 double */ public static BigDecimal mul(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.multiply(b2); } /** * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位,以后的数字四舍五入。 * * @param v1 被除数 * @param v2 除数 * @return 两个参数的商 double */ public static BigDecimal div(double v1, double v2) { return div(v1, v2, DEF_DIV_SCALE); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 定精度,以后的数字四舍五入。 * * @param v1 被除数 * @param v2 除数 * @param scale 表示表示需要精确到小数点以后几位。 * @return 两个参数的商 double */ public static BigDecimal div(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP); } /** * 提供精确的小数位四舍五入处理。 * * @param v 需要四舍五入的数字 * @param scale 小数点后保留几位 * @return 四舍五入后的结果 double */ public static BigDecimal round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(Double.toString(v)); BigDecimal one = new BigDecimal("1"); return b.divide(one, scale, BigDecimal.ROUND_HALF_UP); } }
提供加减乘除、四舍五入的方法。保证精度。返回值类型为保证精度的BigDecimal类型,根据业务需要请转换为自己需要的类型。
下面我们利用此工具类把上面例子再跑一次:
public static void main(String[] args) { System.out.println(add(0.05, 0.01)); //0.06 System.out.println(sub(1.0, 0.42)); //0.58 System.out.println(mul(4.015, 100)); //401.5000 System.out.println(div(123.3, 100)); //1.2330000000 //返回值用double展示 可以保证精度 System.out.println(add(0.05, 0.01).doubleValue()); //0.06 System.out.println(sub(1.0, 0.42).doubleValue()); //0.58 System.out.println(mul(4.015, 100).doubleValue()); //401.5 System.out.println(div(123.3, 100).doubleValue()); //1.233 }
public static void main(String[] args) { Integer i = 1; Long l = 1L; Float f = 1.0f; //这样转换成double类型 或者直接强转 System.out.println(i.doubleValue()); //1.0 System.out.println(l.doubleValue()); //1.0 System.out.println(f.doubleValue()); //1.0 }
如上,若返回值不知道是整数还是小数,请使用double接收。因为double强转为int是会失去精度的
public static void main(String[] args) { System.out.println(add(0.05, 0.01).intValue()); //0 System.out.println(sub(1.0, 0.42).intValue()); //0 System.out.println(mul(4.015, 100).intValue()); //401 System.out.println(div(123.3, 100).intValue()); //1 //double强转为int、long问题 它会直接舍弃小数部分保留整数部分 需要注意 System.out.println((int) 3.41); //3 System.out.println((int) 3.81); //3 }