int加1变负数是因补码表示下2147483647+1得0x80000000,符号位为1,对应-2147483648;java中溢出不抛异常,但math.addexact等可主动检测。

这个问题核心在于:int 是有符号 32 位整数,最大值是 2147483647(即 Integer.MAX_VALUE),再加 1 就会“绕回”到 -2147483648(Integer.MIN_VALUE)。这不是 bug,而是补码表示下硬件的自然行为,但对业务逻辑极具破坏性。
为什么加一就变负数?补码机制决定的
计算机用补码存整数。32 位 int 的最高位是符号位:0 表示正,1 表示负。当 2147483647(0x7FFFFFFF)+1 时,二进制变成 0x80000000 —— 符号位为 1,其余全 0,按补码规则正好对应 -2147483648。这个过程不报错、不中断,程序照常运行,只是数据已错。
关键点:
- Java/C/C++ 中,有符号整型溢出属于未定义行为(C)或明确回绕(Java),但都不抛异常
- 不是所有溢出都变负数——比如
Integer.MIN_VALUE - 1会绕回Integer.MAX_VALUE - 无符号类型(如 C 的
unsigned int)溢出是明确定义的模运算,但 Java 没有无符号基本整型
哪些地方最容易踩中这个坑
常见高危场景不是冷门边缘操作,而是日常代码里的“合理假设”:
-
累加计数器:比如循环中不断
count++,没做上限检查,尤其在处理大量数据或高频事件时 -
金额/积分/ID 计算:例如
salary * months,两个正数相乘结果反成负数(200万 × 12 = -589934592) -
数组索引偏移:
index = base + offset,offset 很大时导致 index 变负,引发ArrayIndexOutOfBoundsException或越界读写 -
时间戳差值计算:如
endTime - startTime,若 startTime 是远古时间(负值大绝对值),差值可能意外溢出
实用防御手段,不止换 long
光靠升级类型治标不治本。真正健壮的做法是分层设防:
-
优先用更宽类型承载中间结果:比如 int 运算前转成 long,算完再校验是否仍在 int 范围内。Java 可用
Math.addExact()、Math.multiplyExact()等方法,溢出直接抛ArithmeticException -
手动前置检查边界:加法前判断
a > Integer.MAX_VALUE - b;乘法前用b != 0 && a > Integer.MAX_VALUE / b(注意除零和负数情况) - 业务层兜底校验:对关键变量(如用户余额、库存数量)在赋值/更新后强制断言非负、非超限,失败则告警或拒绝操作
-
静态分析工具介入:启用 SpotBugs、ErrorProne 或编译器警告(如 GCC 的
-fsanitize=undefined),自动捕获潜在溢出点
特别提醒:除法也有隐藏溢出
很多人忽略这个经典特例:Integer.MIN_VALUE / -1。数学上等于 2147483648,但超出了 int 正数上限,结果仍是 Integer.MIN_VALUE(-2147483648)。Java 标准库的 Math.floorDiv() 和自定义除法必须单独处理该 case。
正确写法示例(Java):
if (dividend == Integer.MIN_VALUE && divisor == -1) {
throw new ArithmeticException("Integer overflow in division");
}
return dividend / divisor;
不复杂但容易忽略











