算法通关村第十三关——java的int溢出

324 阅读5分钟

为什么溢出是个问题

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  ...尾

不易防范,如何避免?

  1. 类型提升: 将int类型提升为long类型,可以扩展整数范围,从而降低溢出的风险。
  2. long型转换为BigDecimal: 如果需要精确计算和存储大整数或小数,可以将long类型的数据转换为BigDecimal。BigDecimal可以提供高精度的算术运算,并且不容易发生溢出。
  3. Math.addExact和Math.multiplyExact: 这两个方法是Java标准库中提供的,用于执行加法和乘法操作,并在溢出时抛出异常,以便及时发现和处理溢出情况。
  4. 最大最小数检查: 在执行计算前,进行最大最小数的检查,以确保计算结果不会超出范围。例如,使用if语句检查是否会超过Integer.MAX_VALUE等。

哪些情况算法题需要条件反射溢出处理

  1. 数学计算和运算
  2. 数组和列表操作
  3. 递归计算
  4. 循环操作
  5. 二进制位操作

例题:整数反转:

//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)。