为什么溢出是个问题
Java的int是32位有符号整数类型,可以表示的范围是从-2147483648到2147483647。当对int类型进行运算时,如果结果超出了这个范围,就会发生溢出,而不会抛出任何异常。
加法例子
System.out.println(Integer.MAX_VALUE + 1); // 输出结果为 -2147483648
System.out.println(Integer.MIN_VALUE - 1); // 输出结果为 2147483647
乘法例子
int num = 0x7FFFFFFF; // 最大的32位整数
int multiplied = num * 2; // 乘以2,结果超出了32位整数范围
System.out.println(multiplied); // 输出 -2
在这个示例中,我们将最大的32位整数 0x7FFFFFFF
乘以2,得到的结果 -2
超出了32位整数的最大值,因此发生了溢出。这个溢出的结果是 -2
,与直接将最大32位整数减去1的结果相同。
这个例子展示了在32位整数范围内,当数值溢出时,它会绕回到对应的边界值。
官方文档对溢出逻辑的解释
IF an integer multiplication overflows, then the result is the low-order bits of the mathematical product as represented in some sufficiently large two's-complement format. As a result, if overflow occurs, then the sign of the result may not be the same as the sign of the mathematical product of the two operand values.
什么是溢出后绕回?(环)
"绕回" 是指当一个数值在超出其数据类型的范围时,它不会引发错误或异常,而是循环回到该数据类型的最小值或最大值。这是整数溢出的常见行为。
举个例子,假设有一个32位整数类型int,其范围是-2147483648到2147483647。如果你在这个范围内的数值上加上1,当结果达到2147483647时,再加1会发生绕回,结果变为-2147483648。类似地,当你在-2147483648上减去1,结果达到最小值-2147483648后,再减1会绕回到2147483647。
//首尾相接成环
首-> 2147483647 -> -2147483648 -> -2147483647 -> ...
-> 0 -> 1 -> 2 -> ... -> 2147483647 -> -2147483648 ...尾
不易防范,如何避免?
- 类型提升: 将int类型提升为long类型,可以扩展整数范围,从而降低溢出的风险。
- long型转换为BigDecimal: 如果需要精确计算和存储大整数或小数,可以将long类型的数据转换为BigDecimal。BigDecimal可以提供高精度的算术运算,并且不容易发生溢出。
- Math.addExact和Math.multiplyExact: 这两个方法是Java标准库中提供的,用于执行加法和乘法操作,并在溢出时抛出异常,以便及时发现和处理溢出情况。
- 最大最小数检查: 在执行计算前,进行最大最小数的检查,以确保计算结果不会超出范围。例如,使用if语句检查是否会超过
Integer.MAX_VALUE
等。
哪些情况算法题需要条件反射溢出处理
- 数学计算和运算
- 数组和列表操作
- 递归计算
- 循环操作
- 二进制位操作
例题:整数反转:
//a<=b 从高到低位比较 a每一位都小于等于b, 一旦发现有某位大于,整个数都大于
public int reverse(int x) {
int res = 0; // 初始化结果变量为0
while(x != 0) { // 当x不为0时,执行循环
int tmp = x % 10; // 取x的最后一位数字
// 判断翻转后的数是否会大于int的最大值
if (res > 214748364 || (res == 214748364 && tmp > 7)) {
return 0; // 如果会溢出,返回0
}
// 判断翻转后的数是否会小于int的最小值
if (res < -214748364 || (res == -214748364 && tmp < -8)) {
return 0; // 如果会溢出,返回0
}
res = res * 10 + tmp; // 将tmp放到res的最后一位
x /= 10; // 去除x的最后一位数字
}
return res; // 返回翻转后的结果
}
为什么要在拼接前判断而不是拼接后
因为一旦溢出发生,就无法通过常规的整数类型检查来恢复或者检测到这个错误。一旦 res * 10 + tmp
的计算结果超出了整数的范围,将得到一个完全不同的、错误的数值,这个数值可能还在 int
的合法范围内,但并不代表实际想要的翻转结果。
例如,如果 res
是214748364(即 Integer.MAX_VALUE / 10
),并且 tmp
是9,那么按照翻转的逻辑,我们希望结果是2147483649。但是,这个值超出了 int
类型能表示的最大值(2147483647)。如果先进行拼接,则会得到一个负数,而这显然不是正确的翻转结果。而且,这个负数可能会在后续操作中被错误地处理。
因此,通过在执行潜在的溢出操作之前进行检查,可以避免这种情况的发生,并确保函数返回一个正确的结果或者一个指示溢出的结果(在这个函数中是0)。