在初学Java的时候,一般我们都会从基本的数据类型开始学习,而在基本数据类型中,我认为double类型是比较难理解的,并且在以后的学习或工作中,在double类型数据这遇到的坑也是极多的。例如下面的这样一个程序
public static void main(String[] args) {
System.out.println(2.0-1.1);
}
很多人会认为上面的程序会打印出0.9
,但实际上,它打印的却是0.8999999999999999
,这是为什么呢?
首先介绍一下,十进制小数是怎么转换为二进制数的,举个例子3.75
首先取出3.75
的小数部分0.75
,将其乘以要转换的进制的进制数,在这里也就是2,乘以2后得到结果1.5
,取1.5
的整数部分作为二进制小数的小数部分的第一位,再取1.5
的小数部分0.5
,乘以2后得到1.0
,将1.0
的整数部分1
作为小数部分的第二位。以此类推,直到最后得到值0
或形成无限循环。3.75
转换为二进制数就是11.11
;
但是我们知道在Java中double类型的数据占8个字节,所以对于无限循环的二进制小数我们只能取到它的近似值,就比如说1.1
。
下面自己写了一个将double类型的数转换为二进制的程序
public static void main(String[] args) {
doubleToBinary(3.75000000);
doubleToBinary(173.8125);
doubleToBinary(1.10);
}/**
* 10任何次负幂都不能精确地被表示为一个长度有限的二进制数
* @param d
*/public static void doubleToBinary(double d) { // 获得double类型的整数部分
int intPart = (int) d;
String tempStr = d + ""; // 获得double类型数的字符串形式的小数部分
String decimalPartStr = tempStr.substring(tempStr.indexOf("."));
BigDecimal decimal = new BigDecimal(decimalPartStr); // 获得小数点后面的位数
int precision = decimal.precision(); // 最终的小数部分二进制字符串
String decimalPartBinary = decimalPartToBinary(Double.parseDouble(decimalPartStr), precision);
System.out.println(Integer.toBinaryString(intPart) + "." + decimalPartBinary);
}/**
* 将小数部分转换为二进制字符串
* @param decimalPart 小数部分
* @param precision 原始数的小数部分位数
@return/public static String decimalPartToBinary(double decimalPart, int precision) { // 转换为整型
long decimalPartLong = (long) (Math.pow(10, precision) * decimalPart);
String temp = ""; int i = 0; while (precision > 0 && i < 64) {
decimalPartLong = decimalPartLong * 2;
temp += (int) (decimalPartLong / Math.pow(10, precision)); //取得除第一位之后的数,并转换为字符串
String str = (long) (decimalPartLong % Math.pow(10, precision)) + ""; if (str.charAt(str.length() - 1) == '0' && str.length() != 1) { //去掉数最后面的0
decimalPartLong = Long.parseLong(str.substring(0, str.length() - 1));
} else {
decimalPartLong = Long.parseLong(str);
} if (decimalPartLong == 0) { break;
}
++i;
precision = (decimalPartLong + "").length();
} return temp;
}
上面程序中的main方法输出的值为
11.1110101101.11011.0001100110011001100110011001100110011001100110011001100110011001
通过上面的程序,我们很容易看到测试数据1.10
在小数部分是无限循环的,1.10
并不能精确地表示为一个double,因此它在Java中被表示为最接近它的double值。既然Java中是对double不能精确表示的数以近似值去存储的,那么在一些需要精确计算的地方就有可能出现错误,甚至产生意想不到的结果。比如说在业务中经常碰到的货币计算。那么如何解决这个问题呢?
1.使用执行精确小数运算的BigDecimal
API,但这里要说明一点,最好(一定)要用BigDecimal(String val)
构造方法,而不要使用BigDecimal(double val)
,因为BigDecimal(double val)
构造方法将会使用它的参数val
的精确值返回一个BigDecimal
,比如new BigDecimal(1.1)
将会返回一个表示1.100000000000000088817841970012523233890533447265625
的BigDecimal
。
近期公司上线的会员项目中,发现有一处double计算后比较大小后没有正确返回true
导致错误的抛出了断言
经过DEBUG后发现,是因为double计算后精度丢失,出现浮点数导致
于是打开测试类进行测试:
public class Test {
public static void main(String[] args) {
double d = 0.05;
double d2 = 0.01;
System.out.println(d+d2);
}
}
得到的结果如下:
0.060000000000000005
可以看到java在计算浮点数的时候,由于二进制无法精确表示0.1的值(就好比十进制无法精确表示1/3一样),所以一般会对小数格式化处理,但是如果涉及到金钱的项目,一点点误差都不能有,必须使用精确运算的时候,就可以使用BigDecimal方法计算.
查看了许多资料,终于找到原因
1.、内存结构
float和double的范围是由指数的位数来决定的。
float的指数位有8位,而double的指数位有11位,分布如下:
image.png
loat:
1bit(符号位) 8bits(指数位) 23bits(尾数位)
image.png
double:
1bit(符号位) 11bits(指数位) 52bits(尾数位)
于是,float的指数范围为-128+127,而double的指数范围为-1024+1023,并且指数位是按补码的形式来划分的。
其中负指数决定了浮点数所能表达的绝对值最小的非零数;富拓跟单而正指数决定了浮点数所能表达的绝对值最大的数,也即决定了浮点数的取值范围。
float的范围为-2^128 ~ +2^127,也即-3.40E+38 ~ +3.40E+38;double的范围为-2^1024 ~ +2^1023,也即-1.79E+308 ~ +1.79E+308。
- 精度
======
float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。
float:2^23 = 8388608,一共七位,由于最左为1的一位省略了,这意味着最多能表示8位数: 2_8388608 = 16777216 。有8位有效数字,但绝对能保证的为7位,也即__float的精度为7~8位有效数字__;
double:2^52 = 4503599627370496,一共16位,同理,__double的精度为16~17位_*。
浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。往往产生误差不是 因为数的大小,而是因为数的精度。因此,产生的结果接近但不等于想要的结果。尤其在使用 float 和 double 作精确运 算的时候要特别小心。
可以考虑采用一些替代方案来实现。如通过 String 结合 BigDecimal 或 者通过使用 long 类型来转换。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。