本文主要来介绍下计算机计算浮点数的问题;计算机使用二进制来计算和存储小数,由于某些小数不能用二进制准确的表示,所以往往有误差。
参考如下测试用例:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
double result = 2.0 - 1.1;
// 存在精度问题
System.out.println("2.0 - 1.1 = " + result);
}
}
输出结果是:
2.0 - 1.1 = 0.8999999999999999
我们知道出现这种问题是因为计算机的二进制不能精确表示1.1;
解决方案可参考《Java解惑》:
1、先把小数转换为整数计算,计算完成再转回小数;
2、使用BigDecimal进行计算。
如果我们再进一步,会发现以下问题:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
double result = 2.0 - 1.1;
// 存在精度问题
System.out.println("2.0 - 1.1 = " + result);
result = 1.0 - 0.1;
// 不存在精度问题
System.out.println("1.0 - 0.1 = " + result);
}
}
输出结果出乎意外:
2.0 - 1.1 = 0.8999999999999999
1.0 - 0.1 = 0.9
请注意“2.0 - 1.1”出现了精度问题;但是“1.0 - 0.1”却没问题?
理论上计算机不能用二进制计算和表示1.1,同样也不能用二进制计算和表示0.1,那为什么“2.0 - 1.1”会出现精度问题,而“1.0 - 0.1”却没问题?
于是我们先看一下计算机是如何表示小数的二进制的,参考下面测试用例:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
calBinary(2.0);
calBinary(1.1);
calBinary(1.0);
calBinary(0.1);
}
/**
* 打印double的二进制表示
*/
private static void calBinary(double d) {
System.out.print(d + "的二进制: ");
long value = Double.doubleToRawLongBits(d);
// 计算double的64位二进制编码,此处注意java默认是Big endian
for (int i = 63; i >= 0; i--) {
int v = (int) ((value >> i) & (0x1));
System.out.print(v);
}
System.out.println();
}
}
输出结果是:
2.0的二进制: 0100000000000000000000000000000000000000000000000000000000000000
1.1的二进制: 0011111111110001100110011001100110011001100110011001100110011010
1.0的二进制: 0011111111110000000000000000000000000000000000000000000000000000
0.1的二进制: 0011111110111001100110011001100110011001100110011001100110011010
java编译器是遵照IEEE制定的浮点数表示法来进行float,double运算的。这种结构是一种科学计数法,用符号、指数和尾数来表示,底数定为2(即把一个浮点数表示为尾数乘以2的指数次方再添上符号)。下面是具体的参数:
符号位数 | 阶码位数 | 尾数位数 | 总位数 | |
float | 1 | 8 | 23 | 32 |
double | 1 | 11 | 52 | 64 |
这里我们只关注double类型(float类似),double数值的计算方法是:
- “符号位”0表示正数,1表示负数;
- 计算“阶码位”的值,然后减去1023,结果记为exp;
- “尾数位”前面补“1.”,然后乘以2的exp次方;
我们来看2.0,1.1,1.0,0.1四个小数的表示:
符号位 | 阶码位 | 尾数位 | 计算结果 | |
2.0 | 0 | 10000000000 | 0000000000000000000000000000000000000000000000000000 | +1.0000000000000000000000000000000000000000000000000000*2的1次方 |
1.1 | 0 | 01111111111 | 0001100110011001100110011001100110011001100110011010 | +1.0001100110011001100110011001100110011001100110011010*2的0次方 |
1.0 | 0 | 01111111111 | 0000000000000000000000000000000000000000000000000000 | +1.0000000000000000000000000000000000000000000000000000*2的0次方 |
0.1 | 0 | 01111111011 | 1001100110011001100110011001100110011001100110011010 | +1.1001100110011001100110011001100110011001100110011010*2的-4次方 |
好,下面来模拟下计算机来通过二进制计算“2.0 - 1.1”和“1.0 - 0.1”:
首先把上表的计算结果的指数都转为2的0次方(即1):
2.0 = 10.0000000000000000000000000000000000000000000000000000
1.1 = 01.0001100110011001100110011001100110011001100110011010
1.0 = 1.00000000000000000000000000000000000000000000000000000000
0.1 = 0.00011001100110011001100110011001100110011001100110011010
然后通过以下测试用例来计算“2.0 - 1.1”和“1.0 - 0.1”:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
System.out.print("2.0 - 1.1 = ");
subtractBinary("10.0000000000000000000000000000000000000000000000000000",
"01.0001100110011001100110011001100110011001100110011010");
System.out.print("1.0 - 0.1 = ");
subtractBinary("1.00000000000000000000000000000000000000000000000000000000",
"0.00011001100110011001100110011001100110011001100110011010");
}
/**
* 计算二进制结果
*/
private static void subtractBinary(String subtrahend, String minuend) {
List<Character> result = new ArrayList<Character>();
int tmp = 0;
for (int i = subtrahend.length() - 1; i >= 0; i--) {
char subtrahendChar = subtrahend.charAt(i);
if (subtrahendChar == '.') {
result.add('.');
continue;
}
char minuendChar = minuend.charAt(i);
if (tmp == 1) {
minuendChar += 1;
tmp = 0;
}
if (subtrahendChar < minuendChar) {
subtrahendChar += 2;
tmp = 1;
}
result.add((char) (subtrahendChar - minuendChar));
}
for (int i = result.size() - 1; i >= 0; i--) {
char value = result.get(i);
if (value == '.') {
System.out.print(value);
continue;
}
System.out.print((int) result.get(i));
}
System.out.println();
}
}
计算结果为:
2.0 - 1.1 = 00.1110011001100110011001100110011001100110011001100110
1.0 - 0.1 = 0.11100110011001100110011001100110011001100110011001100110
再把计算的二进制结果转换为double数值,测试用例如下:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
System.out.print("2.0 - 1.1 = ");
showDouble("00.1110011001100110011001100110011001100110011001100110");
System.out.print("1.0 - 0.1 = ");
showDouble("0.11100110011001100110011001100110011001100110011001100110");
}
/**
* 计算二进制结果
*/
private static void showDouble(String binary) {
int index = binary.indexOf('.');
if (index != -1) {
binary = binary.substring(index + 1);
}
BigDecimal result = new BigDecimal("0.0");
BigDecimal tmp = new BigDecimal("1.0");
BigDecimal halfOne = new BigDecimal("0.5");
for (int i = 0, len = binary.length(); i < len; i++) {
tmp = tmp.multiply(halfOne);
if (binary.charAt(i) == '1') {
result = result.add(tmp);
}
}
System.out.println(result);
}
}
测试结果为:
2.0 - 1.1 = 0.8999999999999999111821580299874767661094665527343750
1.0 - 0.1 = 0.89999999999999999444888487687421729788184165954589843750
可以看到“1.0 - 0.1”也是有误差的;
最后把结果转换为double数值,测试用例如下:
/**
* 测试小数计算问题
*
* @author xionghui
* @email xionghui.xh@alibaba-inc.com
* @since 1.0.0
*/
public class DoubleTest {
public static void main(String[] args) {
double result = 0.8999999999999999111821580299874767661094665527343750;
System.out.println("2.0 - 1.1 = " + result);
result = 0.89999999999999999444888487687421729788184165954589843750;
System.out.println("1.0 - 0.1 = " + result);
}
}
测试结果为:
2.0 - 1.1 = 0.8999999999999999
1.0 - 0.1 = 0.9
好了,可以看到“1.0 - 0.1”的计算结果截取时进位了,所以是0.9;而“2.0 - 1.1”的结算结果截取时没有进位,所以是0.8999999999999999。