【问题标题】:How does Java handle integer underflows and overflows and how would you check for it?Java 如何处理整数下溢和溢出以及如何检查它?
【发布时间】:2010-06-08 22:37:16
【问题描述】:

Java 如何处理整数下溢和上溢?

在此基础上,您将如何检查/测试这种情况是否正在发生?

【问题讨论】:

  • 可惜Java没有像is done in C#那样提供对CPU的overflow flag的间接访问。
  • @DrewNoakes 据我所知,C# 不默认为 checked 太糟糕了。我看它用的不多,输入checked { code; } 和调用一个方法差不多。
  • @MaartenBodewes,您可以在汇编程序编译期间将其设置为默认值。 csc /checked ... 或在 Visual Studio 的项目属性窗格中设置属性。
  • @DrewNoakes 好的,很有趣。有点奇怪,它是代码之外的设置。一般来说,无论此类设置如何(可能除了断言),我都希望程序具有相同的行为。
  • @MaartenBodewes,我认为原因是检查的性能开销很大。所以也许你会在调试版本中启用它,然后在发布版本中禁用它,就像许多其他类型的断言一样。

标签: java integer integer-overflow


【解决方案1】:

Java 不会对 int 或 long 原始类型的整数溢出做任何事情,并且忽略正整数和负整数的溢出。

这个答案首先描述了整数溢出,给出了一个例子来说明它是如何发生的,即使是在表达式评估中的中间值,然后给出了资源的链接,这些资源提供了防止和检测整数溢出的详细技术。

导致意外或未检测到溢出的整数运算和表达式是常见的编程错误。意外或未检测到的整数溢出也是一个众所周知的可利用安全问题,尤其是当它影响数组、堆栈和列表对象时。

溢出可以在正或负方向发生,其中正值或负值将超出所讨论的原始类型的最大值或最小值。在表达式或运算求值期间,中间值可能会发生溢出,并影响最终值应在范围内的表达式或运算的结果。

有时负溢出被错误地称为下溢。下溢是当一个值比表示允许的更接近零时发生的情况。下溢发生在整数算术中并且是预期的。当整数计算介于 -1 和 0 或 0 和 1 之间时,会发生整数下溢。小数结果会被截断为 0。这是正常的,并且在整数运算中是预期的,不被视为错误。但是,它可能导致代码抛出异常。如果整数下溢的结果用作表达式中的除数,则一个示例是“ArithmeticException: / by zero”异常。

考虑以下代码:

int bigValue = Integer.MAX_VALUE;
int x = bigValue * 2 / 5;
int y = bigValue / x;

这导致 x 被分配 0 并且随后对 bigValue / x 的评估引发异常“ArithmeticException: / by zero”(即除以零),而不是 y 被分配值 2。

x 的预期结果为 858,993,458,小于最大 int 值 2,147,483,647。但是,计算 Integer.MAX_Value * 2 的中间结果将是 4,294,967,294,它超过了最大 int 值并且根据 2s 补码整数表示为 -2。 -2 / 5 的后续评估评估为 0,它被分配给 x。

将用于计算 x 的表达式重新排列为一个表达式,该表达式在求值时会在乘法之前先除,代码如下:

int bigValue = Integer.MAX_VALUE;
int x = bigValue / 5 * 2;
int y = bigValue / x;

导致 x 被分配 858,993,458 和 y 被分配 2,这是预期的。

bigValue / 5 的中间结果是 429,496,729,它不超过 int 的最大值。随后对 429,496,729 * 2 的评估不超过 int 的最大值,并且预期的结果被分配给 x。然后对 y 的评估不会除以零。 x 和 y 的评估按预期工作。

Java 整数值被存储为 2s 补码有符号整数表示形式并按照其运行。当结果值大于或小于最大或最小整数值时,将产生一个 2 的补码整数值。在没有明确设计为使用 2s 补码行为的情况下,这是最普通的整数算术情况,生成的 2s 补码值将导致编程逻辑或计算错误,如上例所示。一篇出色的维基百科文章在这里描述了 2s 补码二进制整数:Two's complement - Wikipedia

有一些技术可以避免无意的整数溢出。 Techinques 可以分类为使用前置条件测试、向上转换和 BigInteger。

前置条件测试包括检查进入算术运算或表达式的值,以确保这些值不会发生溢出。编程和设计需要创建测试以确保输入值不会导致溢出,然后确定如果输入值出现会导致溢出时该怎么做。

向上转换包括使用更大的原始类型来执行算术运算或表达式,然后确定结果值是否超出整数的最大值或最小值。即使使用向上转换,操作或表达式中的值或某些中间值仍有可能超出向上转换类型的最大值或最小值并导致溢出,这也不会被检测到并且会导致意外和不希望的结果。通过分析或前置条件,当不上转型的预防不可能或不切实际时,可能有可能通过上转型来防止溢出。如果所讨论的整数已经是 long 原始类型,那么 Java 中的原始类型就无法向上转换。

BigInteger 技术包括将 BigInteger 用于算术运算或使用使用 BigInteger 的库方法的表达式。 BigInteger 不会溢出。如有必要,它将使用所有可用内存。它的算术方法通常只比整数运算效率略低。使用 BigInteger 的结果仍有可能超出整数的最大值或最小值,但是在导致结果的算术中不会发生溢出。如果 BigInteger 结果超出所需原始结果类型(例如 int 或 long)的最大值或最小值,编程和设计仍需要确定该怎么做。

卡内基梅隆软件工程学院的 CERT 计划和 Oracle 为安全 Java 编程创建了一套标准。标准中包括防止和检测整数溢出的技术。该标准在此处作为可免费访问的在线资源发布:The CERT Oracle Secure Coding Standard for Java

描述并包含防止或检测整数溢出的编码技术的实际示例的标准部分在这里:NUM00-J. Detect or prevent integer overflow

还提供 The CERT Oracle Secure Coding Standard for Java 的书本形式和 PDF 形式。

【讨论】:

  • 这是最好的答案,因为它清楚地说明了什么是下溢(接受的答案没有),并且还列出了处理上溢/下溢的技术
【解决方案2】:

如果溢出,它会返回到minimum value 并从那里继续。如果它下溢,它会返回到maximum value 并从那里继续。

您可以按如下方式预先检查:

public static boolean willAdditionOverflow(int left, int right) {
    if (right < 0 && right != Integer.MIN_VALUE) {
        return willSubtractionOverflow(left, -right);
    } else {
        return (~(left ^ right) & (left ^ (left + right))) < 0;
    }
}

public static boolean willSubtractionOverflow(int left, int right) {
    if (right < 0) {
        return willAdditionOverflow(left, -right);
    } else {
        return ((left ^ right) & (left ^ (left - right))) < 0;
    }
}

(您可以将int 替换为long 以对long 执行相同的检查)

如果您认为这种情况可能经常发生,请考虑使用可以存储更大值的数据类型或对象,例如long 或者 java.math.BigInteger。最后一个不溢出,实际可用的JVM内存就是极限了。


如果您碰巧已经在使用 Java8,那么您可以使用新的 Math#addExact()Math#subtractExact() 方法,它们会在溢出时抛出 ArithmeticException

public static boolean willAdditionOverflow(int left, int right) {
    try {
        Math.addExact(left, right);
        return false;
    } catch (ArithmeticException e) {
        return true;
    }
}

public static boolean willSubtractionOverflow(int left, int right) {
    try {
        Math.subtractExact(left, right);
        return false;
    } catch (ArithmeticException e) {
        return true;
    }
}

源码可以分别在herehere找到。

当然,您也可以直接使用它们,而不是将它们隐藏在 boolean 实用方法中。

【讨论】:

  • @dhblah,假设 Java 允许 int 的最大值和最小值分别为 +100, -100。如果您将一个 Java 整数加 1,则该过程看起来像这样,因为它溢出了。 98, 99, 100, -100, -99, -98, ...。这更有意义吗?
  • 我建议使用实用方法而不是立即使用代码。实用方法是内在函数,将被机器特定代码替换。快速测试表明 Math.addExact 比复制的方法 (Java 1.8.0_40) 快 30%。
  • @ErikE Math#addExact 是编写 javadocs 时通常使用的语法 - 虽然通常会转换为 Math.addExact,但有时其他形式只是保留
  • If it underflows, it goes back to the maximum value and continues from there. - 您似乎将下溢与负上溢混淆了。整数下溢总是发生(当结果是分数时)。
  • en.wikipedia.org/wiki/Arithmetic_underflow 表示,下溢是计算机程序中的一种情况,其中计算结果是比计算机在其 CPU 内存中实际可以表示的绝对值更小的数字。所以下溢不适用于 Java 整数。 @BalusC
【解决方案3】:

它环绕。

例如:

public class Test {

    public static void main(String[] args) {
        int i = Integer.MAX_VALUE;
        int j = Integer.MIN_VALUE;

        System.out.println(i+1);
        System.out.println(j-1);
    }
}

打印

-2147483648
2147483647

从 java8 开始,java.lang.Math 包具有 addExact()multiplyExact() 之类的方法,当发生溢出时会抛出 ArithmeticException

【讨论】:

  • 好吧!现在,您能回答一下,如何将其检测到复杂的微积分中吗?
  • @Aubin 您很可能必须在每次计算后进行检查,因为计算可能会多次溢出和/或下溢(两者都可能发生在同一个表达式中),给出一个可以通过简单检查的答案喜欢if((A &gt;= 0 &amp;&amp; B &gt;= 0) &amp;&amp; (resultOfAdd &lt; A || resultOfAdd &lt; B)),因为它会回到预期的范围。可能有比这更聪明的方法来检查单个计算。对于两个int,您可以检查它们的long 值之和是否等于它们int 值之和。您可以使用任意精度类型来检查更大的值。
【解决方案4】:

我自己也遇到了这个问题,这是我的解决方案(乘法和加法):

static boolean wouldOverflowOccurwhenMultiplying(int a, int b) {
    // If either a or b are Integer.MIN_VALUE, then multiplying by anything other than 0 or 1 will result in overflow
    if (a == 0 || b == 0) {
        return false;
    } else if (a > 0 && b > 0) { // both positive, non zero
        return a > Integer.MAX_VALUE / b;
    } else if (b < 0 && a < 0) { // both negative, non zero
        return a < Integer.MAX_VALUE / b;
    } else { // exactly one of a,b is negative and one is positive, neither are zero
        if (b > 0) { // this last if statements protects against Integer.MIN_VALUE / -1, which in itself causes overflow.
            return a < Integer.MIN_VALUE / b;
        } else { // a > 0
            return b < Integer.MIN_VALUE / a;
        }
    }
}

boolean wouldOverflowOccurWhenAdding(int a, int b) {
    if (a > 0 && b > 0) {
        return a > Integer.MAX_VALUE - b;
    } else if (a < 0 && b < 0) {
        return a < Integer.MIN_VALUE - b;
    }
    return false;
}

如有错误或可以简化,请随时纠正。我已经用乘法方法做了一些测试,主要是边缘情况,但它仍然可能是错误的。

【讨论】:

  • 除法相对于乘法来说很慢。对于int*int,我认为只需转换为long 并查看结果是否适合int 将是最快的方法。对于long*long,如果将操作数归一化为正数,则可以将每个操作数分成上下 32 位两半,将每一半提升为长(注意符号扩展!),然后计算两个部分乘积 [其中一个上半部分应为零]。
  • 当您说“对于 long*long,如果将操作数标准化为正数...”,您将如何标准化 Long.MIN_VALUE?
  • 如果需要在实际执行计算之前 测试某些内容是否溢出,这些方法可能会很有趣。这对于测试可能很好,例如用于此类计算的用户输入,而不是在发生异常时捕获异常。
【解决方案5】:

我认为你应该使用这样的东西,它被称为 Upcasting:

public int multiplyBy2(int x) throws ArithmeticException {
    long result = 2 * (long) x;    
    if (result > Integer.MAX_VALUE || result < Integer.MIN_VALUE){
        throw new ArithmeticException("Integer overflow");
    }
    return (int) result;
}

您可以在此处进一步阅读: Detect or prevent integer overflow

来源很靠谱。

【讨论】:

    【解决方案6】:

    有一种情况,上面没有提到:

    int res = 1;
    while (res != 0) {
        res *= 2;
    
    }
    System.out.println(res);
    

    将产生:

    0
    

    这里讨论了这个案例: Integer overflow produces Zero.

    【讨论】:

      【解决方案7】:

      默认情况下,Java 的 int 和 long 数学运算会在溢出和下溢时静默回绕。 (根据JLS 4.2.2,首先通过将操作数提升为 int 或 long 来执行对其他整数类型的整数运算。)

      从 Java 8 开始,java.lang.Math 为执行命名操作的 int 和 long 参数提供了 addExactsubtractExactmultiplyExactincrementExactdecrementExactnegateExact 静态方法,抛出溢出时出现 ArithmeticException。 (没有 divideExact 方法——您必须自己检查一种特殊情况 (MIN_VALUE / -1)。)

      从 Java 8 开始,java.lang.Math 还提供 toIntExact 将 long 转换为 int,如果 long 的值不适合 int,则抛出 ArithmeticException。这可能对例如有用使用未经检查的 long 数学计算 int 的总和,然后使用 toIntExact 最后转换为 int (但注意不要让总和溢出)。

      如果您仍在使用旧版本的 Java,Google Guava 提供了IntMath and LongMath 静态方法,用于检查加法、减法、乘法和求幂(溢出时抛出)。这些类还提供了计算在溢出时返回MAX_VALUE 的阶乘和二项式系数的方法(检查起来不太方便)。 Guava 的基本实用程序类SignedBytesUnsignedBytesShortsInts 提供了用于缩小较大类型的checkedCast 方法(在下/溢出时抛出 IllegalArgumentException,not ArithmeticException),如以及在溢出时返回 MIN_VALUEMAX_VALUEsaturatingCast 方法。

      【讨论】:

        【解决方案8】:

        嗯,就原始整数类型而言,Java 根本不处理 Over/Underflow(对于 float 和 double,其行为不同,它将按照 IEEE-754 的要求刷新到 +/- 无穷大)。

        当添加两个 int 时,您不会在发生溢出时得到任何指示。检查溢出的一个简单方法是使用下一个更大的类型来实际执行操作并检查结果是否仍在源类型的范围内:

        public int addWithOverflowCheck(int a, int b) {
            // the cast of a is required, to make the + work with long precision,
            // if we just added (a + b) the addition would use int precision and
            // the result would be cast to long afterwards!
            long result = ((long) a) + b;
            if (result > Integer.MAX_VALUE) {
                 throw new RuntimeException("Overflow occured");
            } else if (result < Integer.MIN_VALUE) {
                 throw new RuntimeException("Underflow occured");
            }
            // at this point we can safely cast back to int, we checked before
            // that the value will be withing int's limits
            return (int) result;
        }
        

        替代 throw 子句的操作取决于您的应用程序要求(抛出、刷新到最小/最大值或只记录任何内容)。如果您想检测长操作的溢出,那么您对原语不走运,请改用 BigInteger。


        编辑(2014-05-21):由于这个问题似乎经常被提及,而我必须自己解决同样的问题,所以很容易通过 CPU 计算其 V 的相同方法来评估溢出条件标志。

        它基本上是一个布尔表达式,涉及两个操作数的符号以及结果:

        /**
         * Add two int's with overflow detection (r = s + d)
         */
        public static int add(final int s, final int d) throws ArithmeticException {
            int r = s + d;
            if (((s & d & ~r) | (~s & ~d & r)) < 0)
                throw new ArithmeticException("int overflow add(" + s + ", " + d + ")");    
            return r;
        }
        

        在 java 中,将表达式(在 if 中)应用于整个 32 位更简单,并使用 所有整数基元类型,原理完全相同,将上述方法中的所有声明更改为 long 即可使其长期工作。

        对于较小的类型,由于隐式转换为 int(有关详细信息,请参阅 JLS 的按位运算),而不是检查

        /**
         * Subtract two short's with overflow detection (r = d - s)
         */
        public static short sub(final short d, final short s) throws ArithmeticException {
            int r = d - s;
            if ((((~s & d & ~r) | (s & ~d & r)) & 0x8000) != 0)
                throw new ArithmeticException("short overflow sub(" + s + ", " + d + ")");
            return (short) r;
        }
        

        (注意上面的例子使用了subtract溢出检测的表达式)


        那么这些布尔表达式如何/为什么起作用?首先,一些逻辑思考表明,如果两个参数的符号相同,则会发生溢出。因为,如果一个参数是负数,一个是正数,则(添加的)结果必须更接近于零,或者在极端情况下,一个参数为零,与另一个参数相同。由于参数本身不能产生溢出条件,它们的总和也不能产生溢出。

        如果两个参数的符号相同,会发生什么?让我们看一下两者都是正数的情况:添加两个参数来创建大于类型 MAX_VALUE 的总和,将始终产生负值,因此 if arg1 + arg2 > MAX_VALUE 发生溢出。现在可能产生的最大值将是 MAX_VALUE + MAX_VALUE(极端情况下两个参数都是 MAX_VALUE)。对于一个表示 127 + 127 = 254 的字节(示例)。查看添加两个正值可能产生的所有值的位表示,我们会​​发现溢出的那些(128 到 254)都设置了位 7,而所有不溢出的(0 到 127)都将第 7 位(最上面的符号)清除。这正是表达式的第一(右)部分检查的内容:

        if (((s & d & ~r) | (~s & ~d & r)) < 0)
        

        (~s & ~d & r) 变为真,仅当,两个操作数 (s, d) 为正且结果 (r) 为负(表达式适用于所有 32 位, 但我们感兴趣的唯一位是最高(符号)位,它由

        现在,如果两个参数都是负数,那么它们的总和永远不会比任何参数更接近零,总和必须更接近负无穷大。我们可以产生的最极端的值是 MIN_VALUE + MIN_VALUE,它(再次以字节为例)表明对于任何范围内的值(-1 到 -128),符号位都已设置,而任何可能的溢出值(-129 到 -256 ) 已清除符号位。所以结果的符号再次揭示了溢出情况。这就是左半部分 (s & d & ~r) 检查两个参数 (s, d) 为负且结果为正的情况。逻辑在很大程度上等同于肯定的情况;将两个负值相加可能导致的所有位模式都将清除符号位当且仅当发生下溢。

        【讨论】:

        【解决方案9】:
        static final int safeAdd(int left, int right)
                         throws ArithmeticException {
          if (right > 0 ? left > Integer.MAX_VALUE - right
                        : left < Integer.MIN_VALUE - right) {
            throw new ArithmeticException("Integer overflow");
          }
          return left + right;
        }
        
        static final int safeSubtract(int left, int right)
                         throws ArithmeticException {
          if (right > 0 ? left < Integer.MIN_VALUE + right
                        : left > Integer.MAX_VALUE + right) {
            throw new ArithmeticException("Integer overflow");
          }
          return left - right;
        }
        
        static final int safeMultiply(int left, int right)
                         throws ArithmeticException {
          if (right > 0 ? left > Integer.MAX_VALUE/right
                          || left < Integer.MIN_VALUE/right
                        : (right < -1 ? left > Integer.MIN_VALUE/right
                                        || left < Integer.MAX_VALUE/right
                                      : right == -1
                                        && left == Integer.MIN_VALUE) ) {
            throw new ArithmeticException("Integer overflow");
          }
          return left * right;
        }
        
        static final int safeDivide(int left, int right)
                         throws ArithmeticException {
          if ((left == Integer.MIN_VALUE) && (right == -1)) {
            throw new ArithmeticException("Integer overflow");
          }
          return left / right;
        }
        
        static final int safeNegate(int a) throws ArithmeticException {
          if (a == Integer.MIN_VALUE) {
            throw new ArithmeticException("Integer overflow");
          }
          return -a;
        }
        static final int safeAbs(int a) throws ArithmeticException {
          if (a == Integer.MIN_VALUE) {
            throw new ArithmeticException("Integer overflow");
          }
          return Math.abs(a);
        }
        

        【讨论】:

        • 这处理测试。虽然没有解释Java如何处理整数下溢和上溢(添加一些文字解释)。
        【解决方案10】:

        我觉得应该没问题。

        static boolean addWillOverFlow(int a, int b) {
            return (Integer.signum(a) == Integer.signum(b)) && 
                    (Integer.signum(a) != Integer.signum(a+b)); 
        }
        

        【讨论】:

          【解决方案11】:

          有一些库提供安全的算术运算,可以检查整数上溢/下溢。例如,Guava 的 IntMath.checkedAdd(int a, int b) 返回 ab 之和,前提是它不会溢出,如果 a + b 在有符号 int 算术中溢出,则抛出 ArithmeticException

          【讨论】:

          • 是的,这是个好主意,除非您是 Java 8 或更高版本,在这种情况下,Math 类包含类似的代码。
          【解决方案12】:

          它没有做任何事情——下溢/溢出只是发生了。

          作为溢出计算结果的“-1”与从任何其他信息产生的“-1”没有什么不同。因此,您无法通过某种状态或仅检查一个值来判断它是否溢出。

          但您可以巧妙地处理您的计算,以避免溢出,如果它很重要,或者至少知道它什么时候会发生。你的情况如何?

          【讨论】:

          • 这不是一个真正的情况,只是我很好奇并让我思考的事情。如果您需要一个示例用例,这里有一个:我有一个类,它有自己的内部变量,称为“秒”。我有两种方法,它们将整数作为参数,并且会增加或减少(分别)“秒”这么多。您将如何对正在发生的下溢/溢出进行单元测试以及如何防止它发生?
          猜你喜欢
          • 2019-05-17
          • 2011-08-12
          • 2013-06-29
          • 2016-01-05
          • 1970-01-01
          • 2017-03-25
          • 1970-01-01
          相关资源
          最近更新 更多