浮点数运算精度丢失
浮点数 float 或 double 运算时的精度丢失
浮点数 float 或 double 运算时的精度问题场景
先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算:
public class DoubleAndFloatPrecision {
public static void main(String[] args) {
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(5.015*100);
System.out.println(123.3/100);
double doubleExample1 = 2.15;
double doubleExample2 = 1.10;
boolean doubleResult = doubleExample1 - doubleExample2 == 1.05;
System.out.println("double类型计算结果精度是否准确: " + doubleResult);
System.out.println("double类型计算结果为: " + (doubleExample1 - doubleExample2));
float floatExample1 = 2.15f;
float floatExample2 = 1.10f;
boolean floatResult = floatExample1 - floatExample2 == 1.05;
System.out.println("float类型计算结果为: " + (floatExample1 - floatExample2));
System.out.println("float类型计算结果精度是否准确: " + floatResult);
}
}
输出结果如下:
0.30000000000000004
0.19999999999999996
501.49999999999994
1.2329999999999999
double类型计算结果精度是否准确: false
double类型计算结果为: 1.0499999999999998
float类型计算结果为: 1.0500001
float类型计算结果精度是否准确: false
可以看到,输出结果和我们预期的很不一样。是什么原因导致产生该问题呢
浮点数 float 或 double 运算时的精度问题产生原因
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,浮点数也不例外。计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,类似于0.1这中 无法精确表达的数据,就是浮点数计算造成精度损失的根源。
浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断
考虑浮点数舍入和格式化的方式
我们通过String.format 的格式化舍入方式对同为浮点数的数据进行舍入和格式化,最终得到的结果也不一定相同,可以根据下列代码分析下为什么:
double test1 = 5.35;
float test2 = 5.35f;
//四舍五入
System.out.println("double类型的5.35四舍五入数据为: " + String.format("%.1f", test1));
System.out.println("float类型的5.35四舍五入数据为: " + String.format("%.1f", test2));
代码执行结果如下:
double类型的5.35四舍五入数据为: 5.4
float类型的5.35四舍五入数据为: 5.3
产生该问题的原因就是由精度问题和舍入方式共同导致的,double 和 float 的 5.35 其实相当于:
5.350000000000000088817841970012523233890533447265625
5.349999904632568359375
String.format 采用四舍五入的方式进行舍入,取 1 位小数,double 的 3.350 四舍五入为 3.4,而 float 的 3.349 四舍五入为 3.3。
BigDecimal 的应用
BigDecimal应用注意事项
使用 BigDecimal 表示和计算浮点数,务必使用字符串的构造方法来初始化 BigDecimal
当我们代码中接收外部系统的数据为浮点数时,我们直接将浮点数数据转为BigDecimal,代码如下:
public class BigDecimalPrecision {
public static void main(String[] args) {
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.2)));
System.out.println(new BigDecimal(5.015).multiply(new BigDecimal(100)));
System.out.println(new BigDecimal(123.4).divide(new BigDecimal(100)));
}
}
代码执行结果如下:
0.3000000000000000166533453693773481063544750213623046875
0.799999999999999988897769753748434595763683319091796875
501.49999999999996802557689079549163579940795898437500
1.23400000000000005684341886080801486968994140625
根据代码执行结果可以分析出运算结果还是不精确,只不过是精度变高;
根据代码结果分析得出BigDecimal使用的第一个注意事项:使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal
查看下改进的代码
public class BigDecimalPrecisionOptimize {
public static void main(String[] args) {
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.2")));
System.out.println(new BigDecimal("5.015").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("123.4").divide(new BigDecimal("100")));
}
}
代码结果如下:
0.3
0.8
501.500
1.234
浮点数的字符串格式化也要通过 BigDecimal 进行
举例:一个Double类型数据,通过使用本身的Double.toString()方法,转化成BigDecimal类型数据精度是否存在问题?
System.out.println(new BigDecimal("5.015").multiply(new BigDecimal(Double.toString(100))));
代码执行结果如下:
501.5000
根据代码执行结果与上文中得出的结果精度不一致,这是因为BigDecimal 有 scale 和 precision 的概念,scale 表示小数点右边的位数,而 precision 表示精度,也就是有效数字的长度。
new BigDecimal(Double.toString(100)) 得到的 BigDecimal 的 scale=1、precision=4;而 new BigDecimal(“100”) 得到的 BigDecimal 的 scale=0、precision=3。对于 BigDecimal 乘法操作,返回值的 scale 是两个数的 scale 相加。所以,初始化 100 的两种不同方式,导致最后结果的 scale 分别是 4 和 3。最终得出的结果精度错误。
举例:BigDecimal 来格式化数字 分别使用向下舍入和四舍五入方式取 1 位小数进行格式化:
BigDecimal test1 = num1.setScale(1, BigDecimal.ROUND_DOWN);
BigDecimal test2 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
BigDecimal的等值比较问题
BigDecimal的等值比较时,如果我们希望只比较 BigDecimal 的 value,不确定数据精度时,不要使用equals()方法,应使用compareTo() 方法,因为equals() 方法不仅仅会比较值的大小(value)还会比较精度(scale),而 compareTo() 方法比较的时候会忽略精度。
两个BigDecimal类型数据大小的比较
compareTo() : 返回 -1 表示 a 小于 b,0 表示 a 等于 b , 1 表示 a 大于 b
代码如下:
BigDecimal bigDecimalTest1 = new BigDecimal("0.2") ;
BigDecimal bigDecimalTest2 = new BigDecimal("0.3") ;
BigDecimal bigDecimalTest3 = new BigDecimal("0.3") ;
System.out.println("bigDecimalTest1是否比bigDecimalTest2大:" + bigDecimalTest1.compareTo(bigDecimalTest2));
System.out.println("bigDecimalTest1是否比bigDecimalTest2小:" + bigDecimalTest2.compareTo(bigDecimalTest1));
System.out.println("bigDecimalTest3是否与bigDecimalTest2相等:" + bigDecimalTest3.compareTo(bigDecimalTest2));
代码执行结果如下:
bigDecimalTest1是否比bigDecimalTest2大:-1
bigDecimalTest1是否比bigDecimalTest2小:1
bigDecimalTest3是否与bigDecimalTest2相等:0
BigDecimal转为String
数值溢出问题
基本类型的内存溢出
举例:对 long 的最大值进行 +1 操作,会发现最终结果展示为一个负数,long类型数据的最小值。显然这是发生了溢出,而且是默默地溢出,并没有任何异常。这种情况有两种解决方案:
考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法
Math 类的xxExact 方法进行数值运算时,这些方法可以在数值溢出时主动抛出异常。
使用BigInteger和BigDecimal。
BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。