Java 如何处理整数下溢和上溢?
从那开始,您将如何检查/测试这种情况是否正在发生?
如果溢出,它会回到最小值并从那里继续。如果它下溢,它会回到最大值并从那里继续。
您可以按如下方式预先检查:
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;
}
}
当然,您也可以立即使用它们,而不是将它们隐藏在boolean
实用程序方法中。
好吧,就原始整数类型而言,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 位更简单,并使用 < 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;
}
(请注意,上面的示例使用表达式需要进行减法溢出检测)
那么这些布尔表达式如何/为什么起作用?首先,一些逻辑思维表明只有当两个参数的符号相同时才会发生溢出。因为,如果一个参数是负数,一个正数,则(add)的结果必须更接近于零,或者在极端情况下,一个参数为零,与另一个参数相同。由于参数本身无法创建溢出条件,因此它们的总和也无法创建溢出。
那么如果两个参数具有相同的符号会发生什么?让我们看一下两者都是正数的情况:添加两个创建的总和大于类型 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) 为负且结果为正的情况。逻辑在很大程度上等同于肯定的情况;当且仅当发生下溢时,添加两个负值可能导致的所有位模式都将清除符号位。
默认情况下,Java 的 int 和 long 数学在溢出和下溢时默默地环绕。(根据JLS 4.2.2,通过首先将操作数提升为 int 或 long 来执行对其他整数类型的整数运算。)
从 Java 8 开始,为执行命名操作的 int 和 long 参数java.lang.Math
提供addExact
、subtractExact
、multiplyExact
、和static 方法,在溢出时抛出 ArithmeticException incrementExact
。(没有 divideExact 方法——您必须自己检查一种特殊情况 ( )。)decrementExact
negateExact
MIN_VALUE / -1
从 Java 8 开始,java.lang.Math 还提供toIntExact
将 long 转换为 int,如果 long 的值不适合 int,则抛出 ArithmeticException。这对于例如使用未经检查的 long 数学计算 int 的总和,然后toIntExact
在最后使用 to 强制转换为 int 很有用(但要注意不要让总和溢出)。
如果您仍在使用旧版本的 Java,Google Guava 提供了IntMath 和 LongMath静态方法,用于检查加法、减法、乘法和求幂(溢出时抛出)。这些类还提供了计算MAX_VALUE
溢出时返回的阶乘和二项式系数的方法(检查起来不太方便)。Guava 的原始实用程序类 、SignedBytes
和UnsignedBytes
提供了缩小较大类型的方法(在下/溢出时抛出 IllegalArgumentException,Shorts
而不是ArithmeticException),以及在溢出时返回或返回的方法。Ints
checkedCast
saturatingCast
MIN_VALUE
MAX_VALUE
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:/被零”(即除以零),而不是 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 补码二进制整数:二进制补码 - 维基百科
有一些技术可以避免无意的整数溢出。Techinques 可以分类为使用前置条件测试、向上转换和 BigInteger。
前置条件测试包括检查进入算术运算或表达式的值,以确保这些值不会发生溢出。编程和设计需要创建测试以确保输入值不会导致溢出,然后确定如果输入值出现会导致溢出时该怎么做。
向上转换包括使用更大的原始类型来执行算术运算或表达式,然后确定结果值是否超出整数的最大值或最小值。即使使用向上转换,操作或表达式中的值或某些中间值仍然可能超出向上转换类型的最大值或最小值并导致溢出,这也将不会被检测到,并会导致意外和不希望的结果。通过分析或前置条件,当没有向上转换的预防不可能或不切实际时,可能有可能通过向上转换来防止溢出。如果所讨论的整数已经是 long 原始类型,那么 Java 中的原始类型无法向上转换。
BigInteger 技术包括使用 BigInteger 进行算术运算或使用使用 BigInteger 的库方法的表达式。BigInteger 不会溢出。如有必要,它将使用所有可用内存。它的算术方法通常只比整数运算效率略低。使用 BigInteger 的结果仍有可能超出整数的最大值或最小值,但是在导致结果的算术中不会发生溢出。如果 BigInteger 结果超出所需原始结果类型(例如 int 或 long)的最大值或最小值,编程和设计仍需要确定要做什么。
Carnegie Mellon Software Engineering Institute 的 CERT 计划和 Oracle 为安全 Java 编程创建了一套标准。标准中包括防止和检测整数溢出的技术。该标准在此处作为可免费访问的在线资源发布:适用于 Java 的 CERT Oracle 安全编码标准
描述并包含用于防止或检测整数溢出的编码技术的实际示例的标准部分在这里:NUM00-J。检测或防止整数溢出
还提供 CERT Oracle Secure Coding Standard for Java 的书本形式和 PDF 形式。
我自己也遇到了这个问题,这是我的解决方案(乘法和加法):
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;
}
如果有错误或可以简化,请随时纠正。我已经用乘法方法做了一些测试,主要是边缘情况,但它仍然可能是错误的。
它环绕。
例如:
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
有一些库提供安全的算术运算,检查整数上溢/下溢。例如,Guava 的IntMath.checkedAdd(int a, int b)返回 and 的a
和b
,前提是它没有溢出,ArithmeticException
如果a + b
在有符号int
算术中溢出则抛出。
我认为你应该使用这样的东西,它被称为 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;
}
您可以在此处进一步阅读: 检测或防止整数溢出
这是相当可靠的来源。
它没有做任何事情——下溢/溢出只是发生了。
作为溢出计算结果的“-1”与从任何其他信息产生的“-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);
}
有一种情况,上面没有提到:
int res = 1;
while (res != 0) {
res *= 2;
}
System.out.println(res);
将产生:
0
此处讨论了这种情况: 整数溢出产生零。
我认为这应该没问题。
static boolean addWillOverFlow(int a, int b) {
return (Integer.signum(a) == Integer.signum(b)) &&
(Integer.signum(a) != Integer.signum(a+b));
}