Java如何处理整数下溢和溢出,您将如何检查它?


226

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

由此,您将如何检查/测试这种情况的发生?


28
这太糟糕了,Java不提供对CPU的间接访问溢出标志,因为在C#实现
德鲁·诺阿克斯

@DrewNoakes而且checked,据我所知C#没有默认是很糟糕的。我看不到它用的多,打字checked { code; }几乎和调用方法一样多。
Maarten Bodewes,2016年

2
@MaartenBodewes,您可以在汇编程序编译期间将其设置为默认值。csc /checked ...或在Visual Studio的项目属性窗格中设置属性。
Drew Noakes

@DrewNoakes好吧,有趣。虽然这是代码之外的设置,但有些奇怪。总的来说,无论此类设置如何,我都希望程序具有相同的行为(可能断言除外)。
Maarten Bodewes

@MaartenBodewes,我认为原因是检查存在不小的性能开销。因此,也许您可​​以在调试版本中启用它,然后在发行版本中禁用它,就像许多其他类型的断言一样。
Drew Noakes

Answers:


217

如果溢出,它将返回最小值并从那里继续。如果下溢,它将返回最大值并从那里继续。

您可以按以下方式事先检查:

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;
    }
}

(您可以替换intlong对执行相同的检查long

如果您认为这种情况可能发生的次数更多,那么请考虑使用可以存储较大值(例如long或)的数据类型或对象java.math.BigInteger。最后一个不会溢出,实际上,可用的JVM内存是限制。


如果您碰巧已经在Java8上使用了,那么可以使用new 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;
    }
}

源代码分别在此处此处

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


13
@dhblah,假设Java允许int的最大值和最小值+100, -100分别为。如果将一个整数添加到Java整数中,则该过程看起来像这样,因为它溢出了。98, 99, 100, -100, -99, -98, ...。这更有意义吗?
奥斯丁

6
我建议使用实用程序方法而不是立即使用代码。实用程序方法是内部函数,将由机器特定的代码代替。快速测试显示Math.addExact比复制的方法(Java 1.8.0_40)快30%。
TilmannZ

1
@ErikE Math#addExact是编写javadocs时通常使用的语法-通常会将其转换为Math.addExact,但有时其他形式也会停留在该位置
Pokechu22

1
If it underflows, it goes back to the maximum value and continues from there.-您似乎混淆了下溢和负溢出。整数下溢一直发生(当结果是分数时)。
中殿

1
en.wikipedia.org/wiki/Arithmetic_underflow说,下溢是计算机程序中的一种条件,其中计算结果的绝对值小于计算机在其CPU内存中实际代表的绝对值。因此下溢不适用于Java Integer。@BalusC
蒋经国姚明

66

好吧,就原始整数类型而言,Java根本不处理上溢/下溢(对于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子句的方式取决于您的应用程序要求(throw,刷新到min / max或仅记录任何内容)。如果要在长时间操作中检测到​​溢出,则无法使用原语,请改用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位,并使用<0来检查结果更简单(这将有效地测试符号位)。对于所有整数基本类型,该原理的工作原理完全相同,将上述方法中的所有声明更改为long会使它工作很长时间。

对于较小的类型,由于隐式转换为int(有关详细信息,请参见JLS,有关详细信息),而不是检查<0,该检查需要显式屏蔽符号位(对于短操作数为0x8000,对于字节操作数为0x80,调整转换和适当的参数声明):

/**
 * 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;
}

(请注意,上面的示例使用需要的表达式 减去溢出检测)


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

那么,如果两个参数都具有相同的符号怎么办?让我们看一下两种情况都为正的情况:添加两个参数得出的和大于MAX_VALUE类型的总和,将始终产生负值,因此如果出现以下情况,则会发生溢出: 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位上都有效,但我们感兴趣的唯一位)时才是最高(符号)位,由<0进行检查。

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


1
您可以使用按位运算符进行检查,也可以使用Betterlogic.com/roger/2011/05/…– rogerdpack 2011
6

1
这会起作用,但我认为它将对性能造成不利影响。
象棋棋局2014年

33

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

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

从Java 8开始,java.lang.Math还提供toIntExact了将long转换为int的功能,如果long的值不适合int则抛出ArithmeticException。这对于例如使用未经检查的长数学计算整数之和,然后toIntExact在末尾使用强制转换为整数(但要注意不要让总数溢出)非常有用。

如果您仍在使用Java的旧版本,则Google Guava提供了IntMath和LongMath静态方法来检查加法,减法,乘法和乘幂(抛出溢出)。这些类还提供计算MAX_VALUE溢出时返回的阶乘和二项式系数的方法(检查起来较不方便)。番石榴的原始工具类,SignedBytesUnsignedBytesShortsInts提供checkedCast了较大收窄类型(在/溢出就扔抛出:IllegalArgumentException,方法 ArithmeticException),以及saturatingCast该方法返回MIN_VALUEMAX_VALUE上溢。


32

对于int或long基本类型,Java不会对整数溢出执行任何操作,而忽略带有正整数和负整数的溢出。

该答案首先描述了整数溢出的情况,给出了一个示例(即使在表达式评估中具有中间值)如何发生整数溢出,然后给出了指向资源的链接,这些资源提供了防止和检测整数溢出的详细技术。

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

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

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

考虑以下代码:

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

这导致x分配为0,随后对bigValue / x的求值抛出异常“ ArithmeticException:/除以零”(即除以零),而不是为y赋值2。

x的预期结果将是858,993,458,小于最大整数值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,并按照2s补正负号整数表示运行。当结果值大于或小于最大或最小整数值时,将生成2的补码整数值。在未明确设计为使用2s补码行为的情况下(这是最普通的整数算术情况),如上例所示,所得的2s补码值将导致编程逻辑或计算错误。一篇出色的Wikipedia文章在此处描述了2s 补码二进制整数:二进制补码-Wikipedia

有一些避免意外整数溢出的技术。可以将Techinques归类为使用前提条件测试,转换和BigInteger。

前提条件测试包括检查进入算术运算或表达式的值,以确保这些值不会发生溢出。编程和设计将需要创建测试以确保输入值不会引起溢出,然后确定如果输入值出现会导致溢出的处理方法。

向上转换包括使用较大的原始类型执行算术运算或表达式,然后确定结果值是否超出整数的最大值或最小值。即使使用向上转换,操作或表达式中的值或某些中间值仍可能会超出向上转换类型的最大值或最小值,并导致溢出,也不会检测到溢出,并且会导致意外和不良结果。通过分析或前提条件,当无法进行或没有上演的预防时,有可能防止上演的溢出。如果所讨论的整数已经是长原语类型,则Java中的原语类型无法进行向上转换。

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

卡内基梅隆软件工程学院的CERT程序和Oracle已经为安全Java编程创建了一套标准。标准中包括防止和检测整数溢出的技术。该标准在这里发布为可免费访问的在线资源:适用于Java的CERT Oracle安全编码标准

在此处描述和包含用于防止或检测整数溢出的编码技术的实际示例的标准部分在此处:NUM00-J。检测或防止整数溢出

还提供CERT Oracle Java安全编码标准的书籍表格和PDF表格。


这是最好的答案,因为它清楚说明了下溢是什么(公认的答案不是),并且还列出了处理上溢/下溢的技术
天真

12

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

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位半部分,将每个半部分提升为长整数(请注意符号扩展!),然后计算两个部分乘积[上半部分之一应为零]。
supercat 2014年

当您说“对于long * long,如果将一个操作数归一化为正...”,您将如何对Long.MIN_VALUE进行归一化?
fragorl 2014年

如果需要实际执行计算之前测试某些内容是否溢出这些方法可能会很有趣。对于测试(例如用于这种计算的用户输入)进行测试可能会很好,而不是在发生异常时捕获异常。
Maarten Bodewes,2016年

8

有些库提供安全的算术运算,可以检查整数上溢/下溢。例如,番石榴的IntMath.checkedAdd(int a,int b)返回aand的和b,前提是它不溢出,并且ArithmeticException如果a + b有符号int算术溢出则抛出该异常。


是的,除非您使用的是Java 8或更高版本,否则这是一个好主意,在这种情况下,Math该类包含类似的代码。
Maarten Bodewes,2016年

6

它环绕。

例如:

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

好!现在,您能回答如何将其检测为复杂的微积分吗?
奥宾

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;
}

您可以在此处进一步阅读: 检测或防止整数溢出

这是非常可靠的来源。


3

它什么也没做-下溢/上溢只会发生。

计算溢出的结果“ -1”与任何其他信息产生的“ -1”没有区别。因此,您无法通过某些状态或仅检查一个值是否溢出来判断。

但是,您可以对自己的计算很聪明,以免发生溢出(如果很重要),或者至少知道什么时候发生。你的情况如何


这不是真正的情况,只是我好奇并且让我思考的事情。如果您需要一个示例用例,请使用以下示例:我有一个类,它具有自己的内部变量,称为“ seconds”。我有两种使用整数作为参数的方法,它们分别将“秒”增加或减少那么多。如何对下溢/上溢发生进行单元测试,如何防止发生下溢/上溢?
KushalP 2010年

1
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);
}

2
这负责测试。尽管没有解释Java如何处理整数下溢和溢出(添加一些文本进行解释)。
Spencer Wieczorek

1

我认为应该没问题。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.