哈哈,有点标题党了,公司不会让你赔钱的 ~ 不过据我在这两段实习期间的观察,出现 BigDecimal 精度丢失问题是很常见的现象,因为它的坑实在是太多太多了,稍不留神就背上大锅,基于之前整理的笔记,对 BigDecimal 的底层实现原理以及使用它的注意事项进行刨析, OK!下面进入正题 ~
首先,为什么小数有精度丢失呢?
十进制整数转化为二进制: 整数将被除数每次都除以2,只要除到商为0就可以停止这个过程。永远都不会无限循环
十进制小数转化为二进制: 每次将小数部分乘2,取出整数部分,如果小数部分为0,就可以停止这个过程。但是有概率出现无限循环。
比如0.1,0.1×2=0.2,0.2×2=0.4,0.4×2=0.8,0.8×2=1.6,取出整数部分剩 0.6,0.6×2=1.2,再取出整数部分剩 0.2,这样就又回到之前的步骤,也就是陷入了无限循环 ~
其次,BIgDecimal 为什么可以解决这个问题?
BigDecimal类位于 java.math 包下,用于对超过16位有效位的数进行精确的运算。一般来说,double类型的变量可以处理16位有效数,但实际应用中,如果超过16位,就需要BigDecimal类来操作。
BigDecimal 实际上由两部分组成:
-
BigInteger:用来表示数字的整数部分和小数部分的整数形式。
BigInteger是一个不可变的类,它可以表示任意大小的整数,包括非常大的数。 -
scale:表示小数点后的位数。scale 是一个整数,用于指定数字中小数点的位置。如果 scale 为正数,则表示小数点后有几位;如果 scale 为负数,则表示小数点前有几位。
由此可见,BigDecimal在计算时,实际会把数值扩大10的n次倍,变成一个long型整数进行计算,整数计算时自然可以实现精度不丢失。同时结合精度scale,实现最终结果的计算。
最后,那我们再用 Bigdecimal 有哪些注意事项?
1)System.out.println() 中的数字默认是 double 类型的,double 类型小数计算不精准;
2)使用 BigDecimal 类构造方法传入double类型时,计算的结果也是不精确的!可以传 String 或者调用 BIgDecimal.valueof(double);
3)BigDecimal 都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值,或者直接把需要的运算一直运行到底!
4)使用除法函数在 divide 的时候要设置各种参数,要精确的小数位数和舍入模式,不然会出现报错 ~
5)【强制】如上所示 BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。 说明:equals()方法会比较值和精度(1.0和1.00返回结果为false),而 compareTo() 则会忽略精度。
嗯?那 BIgDecimal 是非使用不可嘛?
其实也不是的,在需要精确的小数计算时再使用 BigDecimal,由于它的底层原理,BigDecimal 的性能比 double 和 float 差很多,尤其是在处理庞大的复杂运算时尤为明显,故一般精度的计算没必要使用 BigDecimal。
但话又说回来,像笔者这两次实习的公司里,涉及钱包、余额扣减计算、借贷等场景,这些不允许出错的场景还是很有必要的!
附:BigDecimal 工具类分享
网上粘的 ~
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 简化BigDecimal计算的小工具类
*/
public class BigDecimalUtil {
/**
* 默认除法运算精度
*/
private static final int DEF_DIV_SCALE = 10;
private BigDecimalUtil() {
}
/**
* 提供精确的加法运算。
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double subtract(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算。
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double divide(double v1, double v2) {
return divide(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double divide(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理。
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = BigDecimal.valueOf(v);
BigDecimal one = new BigDecimal("1");
return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 提供精确的类型转换(Float)
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static float convertToFloat(double v) {
BigDecimal b = new BigDecimal(v);
return b.floatValue();
}
/**
* 提供精确的类型转换(Int)不进行四舍五入
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static int convertsToInt(double v) {
BigDecimal b = new BigDecimal(v);
return b.intValue();
}
/**
* 提供精确的类型转换(Long)
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static long convertsToLong(double v) {
BigDecimal b = new BigDecimal(v);
return b.longValue();
}
/**
* 返回两个数中大的一个值
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 返回两个数中大的一个值
*/
public static double returnMax(double v1, double v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.max(b2).doubleValue();
}
/**
* 返回两个数中小的一个值
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 返回两个数中小的一个值
*/
public static double returnMin(double v1, double v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.min(b2).doubleValue();
}
/**
* 精确对比两个数字
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1
*/
public static int compareTo(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.compareTo(b2);
}
}
题外:在 mysql 中除了用 decimal 还能用什么数据类型?
NUMERIC数据类型
NUMERIC 数据类型是一种精确数字数据类型,其精度在算术运算后保留到最小有效位,numeric 是标准 sql 的数据类型,格式是 numeric(m,n)。
numeric(a,b) 函数有两个参数,前面一个为总的位数,后面一个参数是小数点后的位数,例如 numeric(5,2) 是总位数为 5,小数点后为 2 位的数,也就是说这个字段的整数位最大是 3 位。