杰瑞科技汇

Java中double二进制如何表示与计算?

这是一个非常核心且重要的概念,因为它涉及到计算机如何存储浮点数,以及由此带来的一些精度问题。

Java中double二进制如何表示与计算?-图1
(图片来源网络,侵删)

Java 中的 double 类型遵循 IEEE 754 标准,使用 64位(8字节) 来表示一个浮点数,这64位被划分为三个部分:

  1. 符号位: 1位
    • 0 表示正数
    • 1 表示负数
  2. 指数位: 11位

    用于存储指数部分的值,决定小数点的位置。

  3. 尾数位: 52位

    用于存储有效数字(也称为“ significand” 或 “mantissa”),即数字的精度部分。


各个部分的详细解释

符号位

这是最简单的一部分。

Java中double二进制如何表示与计算?-图2
(图片来源网络,侵删)
  • double 的值是正数,符号位就是 0
  • double 的值是负数,符号位就是 1

示例:

  • 45 的符号位是 0
  • -123.45 的符号位是 1

指数位

这部分稍微复杂一些,它存储的是一个 偏移码,而不是直接的指数值。

  • 偏移量: 对于 double,偏移量是 1023 (即 $2^{11-1} - 1 = 1023$)。
  • 计算方式: 计算机存储的指数值 = 实际指数值 + 1023。
  • 范围: 实际指数值的范围是 -1022 到 1023
    • 最小的指数值 (存储为 00000000000) 实际上是 -1023,但它被保留用于表示特殊值(如零和非数)。
    • 最大的指数值 (存储为 11111111111) 实际上是 1024,它也被保留用于表示无穷大和非数。

这种设计可以同时表示正指数和负指数,而无需额外的符号位。

尾数位

尾数位存储的是数字的“有效数字”,为了节省空间,IEEE 754 标准规定,所有浮点数的二进制表示都采用 科学记数法 的形式,并且其整数部分 永远是 1

  • 规范形式: xxxxx... * 2^E
  • 隐含的“1”: 因为整数部分永远是1,所以这个“1”在存储时被省略了,被称为“隐含位”或“隐藏位”,这使得尾数位可以多存储一位有效数字,从而提高了精度。
  • : 尾数位存储的就是小数点后面的 xxxxx... 部分。

示例: 假设一个数的科学记数法是 101 * 2^5

  • 指数 E5
  • 尾数存储的是 101

转换步骤:将一个 double 转换为二进制

我们以一个具体的数字为例,625

第1步:确定符号位

625 是正数,所以符号位为 0

第2步:将整数和小数部分分别转换为二进制

  • 整数部分 (9):

    • 9 / 2 = 4 ... 余 1
    • 4 / 2 = 2 ... 余 0
    • 2 / 2 = 1 ... 余 0
    • 1 / 2 = 0 ... 余 1
    • 从下往上读,整数部分的二进制是 1001
  • 小数部分 (0.625):

    • 625 * 2 = 1.25 ... 取整数部分 1,剩下 0.25
    • 25 * 2 = 0.5 ... 取整数部分 0,剩下 0.5
    • 5 * 2 = 1.0 ... 取整数部分 1,剩下 0.0 (结束)
    • 从上往下读,小数部分的二进制是 101
  • 组合: 625 的完整二进制形式是 101

第3步:转换为科学记数法形式

我们需要将小数点移动到第一个有效数字的后面。 101 = 001101 * 2^3

101001101,小数点向左移动了 3 位,所以指数 E3

  • 符号位: 0
  • 实际指数: 3
  • 尾数: 001101 (小数点后面的部分)

第4步:计算指数位和尾数位

  • 指数位: 实际指数 3 + 偏移量 1023 = 1026

    • 1026 转换为 11 位二进制:
      • 1026 / 2 = 513 ... 0
      • 513 / 2 = 256 ... 1
      • 256 / 2 = 128 ... 0
      • 128 / 2 = 64 ... 0
      • 64 / 2 = 32 ... 0
      • 32 / 2 = 16 ... 0
      • 16 / 2 = 8 ... 0
      • 8 / 2 = 4 ... 0
      • 4 / 2 = 2 ... 0
      • 2 / 2 = 1 ... 0
      • 1 / 2 = 0 ... 1
    • 从下往上读,得到 10000000010
  • 尾数位: 我们科学记数法中的尾数是 001101,我们需要用 0 填充到 52 位。

    • 尾数是 001101 后面跟 46 个 0

第5步:组合成最终的64位二进制

将三部分按顺序拼接起来:

符号位 (1位) 指数位 (11位) 尾数位 (52位)
0 10000000010 001101 + 00000000000000000000000000000000000000000000000000

最终64位表示: 0 10000000010 0011010000000000000000000000000000000000000000000000


特殊值

IEEE 754 还定义了一些特殊的值,它们通过特定的指数和尾数组合来表示:

  • :
    • 符号位任意 (0 或 1),表示正零或负零。
    • 指数位全为 0
    • 尾数位全为 0
  • 非数:
    • 指数位全为 1
    • 尾数位不全为 0
    • 通常由无效的运算产生,如 0 / 0.0Math.sqrt(-1)
  • 无穷大:
    • 指数位全为 1
    • 尾数位全为 0
    • Double.POSITIVE_INFINITY (符号位为0) 和 Double.NEGATIVE_INFINITY (符号位为1)。

Java 代码示例:查看 double 的二进制表示

你可以使用 Java 的 Double 类来查看一个 double 值的各个部分。

public class DoubleBinaryRepresentation {
    public static void main(String[] args) {
        double d = 9.625;
        printDoubleBinary(d);
        System.out.println("\n--- 一个有问题的例子 ---");
        double problematic = 0.1;
        printDoubleBinary(problematic);
    }
    public static void printDoubleBinary(double value) {
        // 1. 使用 Double.doubleToLongBits 获取64位的 long 表示
        long bits = Double.doubleToLongBits(value);
        System.out.println("十进制 double 值: " + value);
        System.out.println("对应的 long 值: " + bits);
        // 2. 分别提取符号位、指数位和尾数位
        // 使用位操作
        long signBit = (bits >>> 63) & 0x1;
        long exponentBits = (bits >>> 52) & 0x7FF; // 0x7FF 是 11位全1的掩码
        long mantissaBits = bits & 0x0FFFFFFFFFFFFFL; // 52位全1的掩码
        System.out.println("符号位 (1位): " + signBit);
        System.out.println("指数位 (11位): " + Long.toBinaryString(exponentBits));
        System.out.println("尾数位 (52位): " + Long.toBinaryString(mantissaBits));
        // 3. 计算实际的指数值
        int actualExponent = (int) exponentBits - 1023;
        System.out.println("实际指数值: " + actualExponent);
        // 4. 使用 Double 类的方法验证
        System.out.println("\n--- 使用 Double 类方法验证 ---");
        System.out.println("Double.isNaN(" + value + "): " + Double.isNaN(value));
        System.out.println("Double.isInfinite(" + value + "): " + Double.isInfinite(value));
        System.out.println("Double.doubleToRawLongBits(" + value + "): " + Double.doubleToRawLongBits(value));
    }
}

运行结果分析 (对于 625):

十进制 double 值: 9.625
对应的 long 值: 4638116846983948032
符号位 (1位): 0
指数位 (11位): 10000000010
尾数位 (52位): 11010000000000000000000000000000000000000000000000
实际指数值: 3
--- 使用 Double 类方法验证 ---
Double.isNaN(9.625): false
Double.isInfinite(9.625): false
Double.doubleToRawLongBits(9.625): 4638116846983948032

注意,尾数位的输出和我们手动计算的不完全一样,这是因为 Long.toBinaryString 会省略前导零。001101 在52位中存储时,前面有50个零,总共是52位,代码输出的 .. 实际上是 .. 的有效部分。

为什么 double 存在精度问题?

理解了二进制表示,就能明白为什么 1 + 0.2 不等于 3

  • 十进制:0.1 可以精确表示。
  • 二进制:0.1 是一个无限循环小数,它的二进制表示是 000110011001100...
  • 存储限制double 只有 52 位尾数来存储这个无限循环的小数,它必须进行截断或舍入

计算机中存储的 1 实际上是一个最接近 1 的、可以用52位尾数表示的二进制浮点数,这个值本身就带有微小的误差,同理,23 也都是存储的近似值,当这些带有微小误差的数进行运算时,误差会被放大,最终导致结果不精确。

double 的精度问题源于其有限的尾数位无法精确表示某些十进制小数(特别是那些在二进制中是无限循环的数),这是由 IEEE 754 标准决定的,是 double 类型本身的特性,而不是 Java 的 Bug,在需要高精度计算的场景(如金融计算),应使用 java.math.BigDecimal 类。

分享:
扫描分享到社交APP
上一篇
下一篇