Java⽤BigDecimal类解决Double类型精度丢失的问题本篇要点
简单描述浮点数⼗进制转⼆进制精度丢失的原因。
介绍⼏种创建BigDecimal⽅式的区别。
整理了⾼精度计算的⼯具类。
学习了阿⾥巴巴Java开发⼿册关于BigDecimal⽐较相等的规定。
经典问题:浮点数精度丢失
精度丢失的问题是在其他计算机语⾔中也都会出现,float和double类型的数据在执⾏⼆进制浮点运算的时候,并没有提供完全精确的结果。产⽣误差不在于数的⼤⼩,⽽是因为数的精度。
关于浮点数存储精度丢失的问题,话题过于庞⼤,感兴趣的同学可以⾃⾏搜索⼀下:
这⾥简单讨论⼀下⼗进制数转⼆进制为什么会出现精度丢失的现象,⼗进制数分为整数部分和⼩数部分,我们分开来看看就知道原因为何:
⼗进制整数如何转化为⼆进制整数?
将被除数每次都除以2,只要除到商为0就可以停⽌这个过程。
5 / 2 = 2 余 1
2 / 2 = 1 余 0
1 /
2 = 0 余 1
// 结果为 101
这个算法永远都不会⽆限循环,整数永远都可以使⽤⼆进制数精确表⽰,但⼩数呢?
⼗进制⼩数如何转化为⼆进制数?
每次将⼩数部分乘2,取出整数部分,如果⼩数部分为0,就可以停⽌这个过程。
0.1 * 2 = 0.2 取整数部分0
0.2 * 2 = 0.4 取整数部分0
0.4 * 2 = 0.8 取整数部分0
0.8 * 2 = 1.6 取整数部分1
0.6 * 2 = 1.2 取整数部分1
0.2 * 2 = 0.4 取整数部分0
//... 我想写到这就不必再写了,你应该也已经发现,上⾯的过程已经开始循环,⼩数部分永远不能为0
这个算法有⼀定概率会存在⽆限循环,即⽆法⽤有限长度的⼆进制数表⽰⼗进制的⼩数,这就是精度丢失问题产⽣的原因。如何⽤BigDecimal解决double精度问题?
我们已经明⽩为什么精度会存在丢失现象,那么我们就应该知道,当某个业务场景对double数据的精度要求⾮常⾼时,就必须采取某种⼿段来处理这个问题,这也是BigDecimal为什么会被⼴泛应⽤于⾦额⽀付场景中的原因啦。
BigDecimal类位于java.math包下,⽤于对超过16位有效位的数进⾏精确的运算。⼀般来说,double类型的变量可以处理16位有效数,但实际应⽤中,如果超过16位,就需要BigDecimal类来操作。
既然这样,那⽤BigDecimal就能够很好解决这个问题咯?
public static void main(String[] args) {
// ⽅法1
BigDecimal a = new BigDecimal(0.1);
System.out.println("a --> " + a);
// ⽅法2
BigDecimal b = new BigDecimal("0.1");
System.out.println("b --> " + b);
// ⽅法3
BigDecimal c = BigDecimal.valueOf(0.1);
System.out.println("c --> " + c);
}
你可以思考⼀下,控制台输出会是啥。
a --> 0.1000000000000000055511151231257827021181583404541015625
b --> 0.1
c --> 0.1
可以看到,使⽤⽅法⼀的构造函数仍然出现了精度丢失的问题,⽽⽅法⼆和⽅法三符合我们的预期,为什么会这样呢?
这三个⽅法其实对应着三种不同的构造函数:
// 传⼊double
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
// 传⼊string
public BigDecimal(String val) {
CharArray(), 0, val.length());
}
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
// 可以看到实际上就是第⼆种
return new String(val));
}
关于这三个构造函数,JDK已经给出了解释,并⽤Notes标注:
为了防⽌以后图⽚可能会存在显⽰问题,这⾥再记录⼀下:
new BigDecimal(double val)
该⽅法是不可预测的,以0.1为例,你以为你传了⼀个double类型的0.1,最后会返回⼀个值为0.1的BigDecimal吗?不会的,原因在于,0.1⽆法⽤有限长度的⼆进制数表⽰,⽆法精确地表⽰为双精度数,最后的结果会是0.100000xxx。
new BigDecimal(String val)
该⽅法是完全可预测的,也就是说你传⼊⼀个字符串"0.1",他就会给你返回⼀个值完全为0,1的BigDecimal,官⽅也表⽰,能⽤这个构造函数就⽤这个构造函数叭。
BigDecimal.valueOf(double val)
第⼆种构造⽅式已经⾜够优秀,可你还是想传⼊⼀个double值,怎么办呢?官⽅其实提供给你思路并且实现了它,可以使⽤String(double val)先将double值转为String,再调⽤第⼆种构造⽅式,你可以直接使⽤静态⽅法:valueOf(double val)。
Double的加减乘除运算⼯具类
BigDecimal所创建的是对象,故我们不能使⽤传统的+、-、*、/等算术运算符直接对其对象进⾏数学运算,⽽必须调⽤其相对应的⽅法。⽅法中的参数也必须是BigDecimal的对象。⽹上有很多这样的⼯具类,这边直接贴⼀下,逻辑不难,主要为了简化项⽬中频繁互相转化的问题。
/**
* ⽤于⾼精确处理常⽤的数学运算
*/
public class ArithmeticUtils {
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = new String(v1));
BigDecimal b2 = new String(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static BigDecimal add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2);
}
/
**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @param scale 保留scale 位⼩数
* @return 两个参数的和
*/
public static String add(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); }
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = new String(v1));
BigDecimal b2 = new String(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static BigDecimal sub(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2);
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @param scale 保留scale 位⼩数
* @return 两个参数的差
*/
public static String sub(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); }
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = new String(v1));
BigDecimal b2 = new String(v2));
return b1.multiply(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位⼩数
* @return 两个参数的积
*/
public static double mul(double v1, double v2, int scale) {
BigDecimal b1 = new String(v1));
BigDecimal b2 = new String(v2));
return round(b1.multiply(b2).doubleValue(), scale);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位⼩数
* @return 两个参数的积
*/
public static String mul(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); }
/**
* 提供(相对)精确的除法运算,当发⽣除不尽的情况时,精确到
* ⼩数点以后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 new IllegalArgumentException("The scale must be a positive integer or zero"); }
BigDecimal b1 = new String(v1));
BigDecimal b2 = new String(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供(相对)精确的除法运算。当发⽣除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五⼊
*
* @param v1 被除数
* @param v2 除数
* @param scale 表⽰需要精确到⼩数点以后⼏位
* @return 两个参数的商
*/
public static String div(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero"); }
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v1);
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的⼩数位四舍五⼊处理
bigdecimal除法保留小数*
* @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 = new String(v));
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/
**
* 提供精确的⼩数位四舍五⼊处理
*
* @param v 需要四舍五⼊的数字
* @param scale ⼩数点后保留⼏位
* @return 四舍五⼊后的结果
*/
public static String round(String v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 取余数
*
* @param v1 被除数
* @param v2 除数
* @param scale ⼩数点后保留⼏位
* @return 余数
*/
public static String remainder(String v1, String v2, int scale) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论