1. 引言

本文将深入探讨 Java 中数值类型在运算过程中可能发生的溢出(overflow)和下溢(underflow)问题。

我们不会过多涉及底层理论,而是聚焦于 在 Java 中何时会发生这些问题,以及如何识别和处理它们

✅ 主要内容分为两部分:

  • 整数类型(int、long 等)的溢出与下溢
  • 浮点类型(float、double)的行为差异及应对策略

同时,我们还会介绍如何在运行时检测这些问题,避免踩坑。


2. 什么是溢出与下溢

简单来说,当赋给变量的数值超出了其数据类型的表示范围时,就会发生溢出或下溢。

  • 溢出(Overflow):数值绝对值太大,超出上限
  • 下溢(Underflow):数值太小(接近零),低于下限

示例说明

假设你想把 10^1000(一个 1 后面跟着 1000 个 0)赋值给 intdouble 变量 —— 这显然远远超出它们的表示能力,就会发生 溢出

再比如,尝试将 10^-1000 这种极小的数赋给 double,也会因精度不足导致 下溢

接下来我们看 Java 在这些情况下的具体表现。


3. 整数类型的溢出行为

Java 的整数类型包括:byte(8位)、short(16位)、int(32位)、long(64位)。

本节以 int 为例,其他类型逻辑一致,只是取值范围不同。

int 是有符号 32 位整数,取值范围为:

  • 最小值:-2^31 = -2,147,483,648Integer.MIN_VALUE
  • 最大值:2^31 - 1 = 2,147,483,647Integer.MAX_VALUE

3.1 溢出示例

如果对 int 变量执行 MAX_VALUE + 1,会发生什么?

你可能会期望抛出异常或报错,但 Java 的实际行为是:

⚠️ 整数回绕(Integer Wraparound)

即:超过上限后,从最小值重新开始

来看一段代码:

int value = Integer.MAX_VALUE - 1;
for (int i = 0; i < 4; i++, value++) {
    System.out.println(value);
}

输出结果为:

2147483646
2147483647
-2147483648
-2147483647

看到没?从 2147483647 加 1 后,直接跳到了 -2147483648,这就是典型的 整数溢出回绕

同理,MIN_VALUE - 1 会变成 MAX_VALUE

这种行为在某些场景下非常隐蔽,容易引发严重 bug。


4. 如何处理整数溢出

Java 默认不会抛出异常来提示溢出,这使得这类问题很难被发现。而且 JVM 也没有暴露 CPU 的溢出标志位供我们直接访问。

但我们可以主动防御,以下是几种常用方案:

4.1 使用更大范围的数据类型

最简单的办法是升级数据类型:

  • long 替代 int:范围更大(±9.2e18),大多数场景够用
  • BigInteger:理论上无上限,受限于内存

示例:用 BigInteger 避免溢出

BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + "");
for (int i = 0; i < 4; i++) {
    System.out.println(largeValue);
    largeValue = largeValue.add(BigInteger.ONE);
}

输出:

2147483647
2147483648
2147483649
2147483650

✅ 完全没有回绕,安全可靠。

更多关于 BigInteger 的使用技巧,可参考《Java 中的 BigDecimal 与 BigInteger》一文。


4.2 抛出异常:使用精确算术方法(Java 8+)

如果你希望在溢出时直接抛异常,而不是静默回绕,Java 8 提供了 Math.addExact() 等一系列 精确算术方法

示例:Math.addExact

int value = Integer.MAX_VALUE - 1;
for (int i = 0; i < 4; i++) {
    System.out.println(value);
    value = Math.addExact(value, 1); // 溢出时抛异常
}

输出:

2147483646
2147483647
Exception in thread "main" java.lang.ArithmeticException: integer overflow
    at java.lang.Math.addExact(Math.java:790)
    at com.example.OverflowDemo.main(OverflowDemo.java:15)

✅ 一旦溢出,立即中断,便于调试。

其他可用的精确方法(Java 8+)

方法 用途
Math.addExact(a, b) 加法
Math.subtractExact(a, b) 减法
Math.multiplyExact(a, b) 乘法
Math.incrementExact(a) 自增
Math.decrementExact(a) 自减
Math.toIntExact(long) long → int 转换(溢出则抛异常)

精确转换示例

BigInteger largeValue = BigInteger.TEN;
long longValue = largeValue.longValueExact(); // 安全转换
int intValue = largeValue.intValueExact();     // 若超限则抛 ArithmeticException

4.3 Java 8 之前的替代方案

如果你还在用 Java 7 或更早版本,可以自己实现 addExact

public static int addExact(int x, int y) {
    int r = x + y;
    if (((x ^ r) & (y ^ r)) < 0) {
        throw new ArithmeticException("int overflow");
    }
    return r;
}

📌 原理:利用异或和位运算判断符号位是否异常变化,从而检测溢出。

虽然略显晦涩,但这是 JDK 内部的真实实现方式之一。


5. 浮点类型的溢出与下溢

floatdouble 的行为与整数完全不同。Java 遵循 IEEE 754 浮点数标准,因此其溢出机制也遵循该规范。

关键点:

  • ❌ 没有 addExact 这类方法
  • ✅ 支持特殊值:POSITIVE_INFINITYNEGATIVE_INFINITYNaN+0.0-0.0

下面我们重点看 double 类型的溢出与下溢处理。


5.1 浮点溢出(Overflow)

你可能以为 Double.MAX_VALUE + 1 == Double.MIN_VALUE,就像整数那样回绕?

❌ 错!浮点数不会回绕。

来看几个关键事实:

assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);

为什么?因为 double 的精度有限(有效位数有限),加 1 并不足以改变其二进制表示中的任何有效位,所以值不变。

只有当增量足够大,导致指数部分溢出时,才会变成无穷大:

assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);
assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

✅ 总结:浮点溢出有两种结果:

  1. 值不变(因精度不足)
  2. 变为 POSITIVE_INFINITYNEGATIVE_INFINITY

5.2 浮点下溢(Underflow)

double 的最小正值由两个常量定义:

  • Double.MIN_VALUE: 4.9e-324 —— 可表示的最小正非零值
  • Double.MIN_NORMAL: 2.2250738585072014E-308 —— 正规化数的最小值

IEEE 754 允许“非正规化数”(denormal numbers),所以 MIN_VALUEMIN_NORMAL 更小。

但由于位数限制,double 无法表示介于 04.9e-324 之间的数。

下洋试验

for (int i = 1073; i <= 1076; i++) {
    System.out.println("2^" + i + " = " + Math.pow(2, -i));
}

输出:

2^1073 = 1.0E-323
2^1074 = 4.9E-324
2^1075 = 0.0
2^1076 = 0.0

⚠️ 当数值小于 4.9e-324 时,结果被截断为 0.0,这就是 下溢

  • 正数下溢 → +0.0
  • 负数下溢 → -0.0

6. 如何检测浮点溢出与下溢

由于浮点溢出会产生 INFINITY,下溢会产生 ±0.0,我们可以通过判断这些特殊值来检测异常。

自定义 powExact 方法示例

public static double powExact(double base, double exponent) {
    if (base == 0.0) {
        return 0.0;
    }
    
    double result = Math.pow(base, exponent);
    
    if (result == Double.POSITIVE_INFINITY) {
        throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY");
    } else if (result == Double.NEGATIVE_INFINITY) {
        throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY");
    } else if (Double.compare(-0.0, result) == 0) {
        throw new ArithmeticException("Double underflow resulting in negative zero");
    } else if (Double.compare(+0.0, result) == 0) {
        throw new ArithmeticException("Double underflow resulting in positive zero");
    }

    return result;
}

📌 注意:必须使用 Double.compare() 来区分 +0.0-0.0,因为 == 操作符认为它们相等。


7. 正零与负零的区别

虽然 +0.0 == -0.0 返回 true,但在某些运算中它们表现不同:

double a = +0.0;
double b = -0.0;

assertTrue(a == b); // ✅ 相等

assertTrue(1 / a == Double.POSITIVE_INFINITY);  // ✅
assertTrue(1 / b == Double.NEGATIVE_INFINITY);  // ✅

assertTrue(1 / a != 1 / b); // ✅ 成立!

⚠️ 看似矛盾?其实不然。

  • +0.0-0.0 在比较时被视为相等
  • 但参与除法等运算时,符号会被保留,产生不同的无穷大

📌 实际开发中,若涉及极限、科学计算等场景,务必小心处理 ±0.0±Infinity


8. 总结

本文系统梳理了 Java 中数值类型的溢出与下溢问题:

类型 溢出行为 检测方式
整数(int/long) 回绕(wraparound) 使用 Math.addExact() 或升级为 BigInteger
浮点(double/float) 变为 ±Infinity±0.0 检查特殊值,配合 Double.compare()

✅ 关键建议:

  • 对关键业务逻辑,优先使用 Math.*Exact() 方法防止整数溢出
  • 高精度计算场景使用 BigDecimalBigInteger
  • 浮点运算注意 ±0.0±Infinity 的语义差异

示例代码已托管至 GitHub:https://github.com/example/java-overflow-demo


原始标题:Overflow and Underflow in Java