在Java中执行int result = 100000 * 100000;,你期望得到10000000000,实际打印出来的却是一个负数。这不是编译器的bug,而是整数溢出的典型表现。Java的int类型采用32位二进制补码存储,能表示的最大值是2147483647,约21亿。当乘法结果超出这个范围时,Java不会抛出异常,而是直接丢弃最高位之外的进位,导致结果“绕回”到负数区间。这种静默失败非常危险,因为它往往在生产环境运行很久才被发现。本文将从原理出发,逐个分析几种可行的解决方案。

一、溢出到底是怎么发生的
要理解溢出,先要看Java中整数的存储方式。int类型占4个字节共32位,其中最高位是符号位。以二进制补码表示时,int的取值范围是-2147483648到2147483647。两个int相乘,数学上的结果可能需要最多64位才能表示,但Java的运算规则是:结果只保留低32位,超出部分直接丢弃。
举个例子,计算100000 * 100000,正确结果10000000000的二进制表示需要34位,截断后保留的低32位被最高位符号位解读为负数,最终得到1410065408。整个过程没有任何警告,编译器不报错,运行时也不抛异常,这就是溢出问题难以排查的原因。
还需要注意字面量陷阱:long x = 100000 * 100000;同样会溢出,因为等号右边的两个操作数都是int,乘法在int范围内完成之后才赋值给long,为时已晚。正确写法是long x = 100000L * 100000;,让其中一个操作数提升为long,整个运算就会按64位进行。
二、使用Math.multiplyExact主动检测溢出
从Java 8开始,java.lang.Math类提供了一组带Exact后缀的方法,包括multiplyExact、addExact、subtractExact等。这些方法在运算发生溢出时会抛出ArithmeticException,把静默错误变成快速失败,这是处理关键业务计算时最推荐的方式。
public class OverflowDemo {
public static void main(String[] args) {
try {
int result = Math.multiplyExact(100000, 100000);
System.out.println("结果:" + result);
} catch (ArithmeticException e) {
System.out.println("发生溢出:" + e.getMessage());
}
// long版本同样受支持,范围更大但仍可检测
try {
long big = Math.multiplyExact(Long.MAX_VALUE, 2);
} catch (ArithmeticException e) {
System.out.println("long也溢出了");
}
}
}这种方案的优点是语义清晰、开销极小,方法内部通过位运算判断溢出条件,性能几乎无损。缺点是调用方必须处理异常,如果异常处理不当,可能把计算错误变成程序中断。因此它适合那些“宁可失败也不能算错”的场景,例如金融金额计算、库存数量统计、权限配额校验等。
如果业务逻辑希望在溢出时取边界值而不是抛异常,Java 8还提供了Math.toIntExact的兄弟思路,也可以借助第三方库(如Guava的IntMath.checkedMultiply),行为与multiplyExact类似,可按团队技术栈选择。
三、扩大类型范围:升级为long或BigInteger
如果业务上能预知结果可能超出int范围,最直接的办法是提升运算类型。int是32位,long是64位,可表示到约9.2乘以10的18次方,对于绝大多数乘法场景绰绰有余。关键在于让运算一开始就在long空间进行:
int a = 100000; int b = 100000; // 错误写法:右侧已经是int溢出的结果 long wrong = a * b; // 正确写法:先将其中一个操作数转为long long right = (long) a * b; System.out.println(wrong); // 1410065408 System.out.println(right); // 10000000000
注意强转的位置很重要。(long) a * b是先把a提升为long再相乘,而(long) (a * b)是先在int范围内完成乘法再转换,后者依然会溢出。这是代码评审中最常见的笔误之一。
当连long都不够用时,比如计算阶乘、组合数、大数幂运算,就该请出java.math.BigInteger。它内部用int数组存储任意精度的数值,完全不存在溢出概念,代价是对象创建开销和不能直接使用运算符:
import java.math.BigInteger; BigInteger a = BigInteger.valueOf(100000); BigInteger result = a.pow(10); // 100000的10次方,远超long范围 System.out.println(result);
BigInteger适合科学计算、密码学、超大数值统计等场景。但它的性能比原生类型慢一到两个数量级,且写法繁琐,不适合作为日常计算的默认选择。一个常见的工程实践是:对外接口和内部计算用long,只在确实可能出现超大数值的分支才切换到BigInteger。
四、手动预判与防御性编程
在无法使用Java 8新API的老项目中,或者对异常敏感的代码路径里,可以手动预判溢出。核心思路是在乘法执行前检查操作数:如果b不为零,且a超出了Integer.MAX_VALUE / b与Integer.MIN_VALUE / b划定的区间,就必然溢出。JDK中BigInteger的源码以及Math.multiplyExact内部采用的就是类似的判断逻辑:
public static int safeMultiply(int a, int b) {
if (b == 0) return 0;
int limit = Integer.MAX_VALUE / b;
int lower = Integer.MIN_VALUE / b;
if (b > 0) {
if (a > limit || a < lower) {
throw new ArithmeticException("int乘法溢出");
}
} else {
if (a < limit || a > lower) {
throw new ArithmeticException("int乘法溢出");
}
}
return a * b;
}除了运行时手段,防御性设计同样重要。在接口层面对输入做范围校验,比如限制单次购买数量、限制分页参数的size值,可以从源头掐断溢出的可能。单元测试中应专门包含边界用例,例如Integer.MAX_VALUE * 2、-1 * Integer.MIN_VALUE这类经典陷阱,其中后者连multiplyExact都会抛异常,因为MIN_VALUE的绝对值比MAX_VALUE大1。
总结一下选择策略:一般业务计算优先用long并确保运算前完成类型提升;关键金额与计数逻辑使用Math.multiplyExact快速失败;超大数值场景交给BigInteger;老代码则用手动预判兜底。理解溢出原理加上合适的工具选型,就能把这类隐蔽的数值bug彻底挡在线下。
Java整数溢出Math.multiplyExactBigInteger修改时间:2026-09-04 00:32:55