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) {
    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的指数次方再添上符号)。下面是具体的参数:

 符号位数阶码位数尾数位数总位数
float182332
double1115264

这里我们只关注double类型(float类似),double数值的计算方法是:

  1. “符号位”0表示正数,1表示负数;
  2. 计算“阶码位”的值,然后减去1023,结果记为exp;
  3. “尾数位”前面补“1.”,然后乘以2的exp次方;

我们来看2.0,1.1,1.0,0.1四个小数的表示:

 符号位阶码位尾数位计算结果
2.00100000000000000000000000000000000000000000000000000000000000000+1.0000000000000000000000000000000000000000000000000000*2的1次方
1.10011111111110001100110011001100110011001100110011001100110011010+1.0001100110011001100110011001100110011001100110011010*2的0次方
1.00011111111110000000000000000000000000000000000000000000000000000+1.0000000000000000000000000000000000000000000000000000*2的0次方
0.10011111110111001100110011001100110011001100110011001100110011010+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。

 

转载于:https://my.oschina.net/xionghui/blog/821594

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值