正如其他人指出的那样,Scala 和 Java 默认情况下不对整数值执行溢出检查。 (我同意悄悄地溢出一个值会导致意想不到的——和不想要的——行为,以及许多非常微妙的错误。)然而,一切都没有丢失。 java.lang.Math 具有静态方法(addExact、incrementExact、multiplyExact、subtractExact 等),如果计算溢出基础类型,则会抛出 ArithmeticException。例如:
scala> def factorial(n: Int): Int = {
| if(n <= 1) 1
| else Math.multiplyExact(n, factorial(n - 1))
| }
factorial: (n: Int)Int
scala> factorial(12)
res0: Int = 479001600
scala> factorial(13)
java.lang.ArithmeticException: integer overflow
at java.lang.Math.multiplyExact(Math.java:867)
at .factorial(<console>:13)
... 28 elided
这个版本不是尾递归的,抛出异常违反函数式编程原则(更好的方法是返回一个封装了可能的错误的类型,例如Try[Int]或Either[Throwable, Int]) ,但它会检测并报告Int 类型的溢出。详情请参阅java.lang.Math。
例如,这是一个不会耗尽堆栈的功能版本:
scala> import scala.annotation.tailrec
import scala.annotation.tailrec
scala> import scala.util.Try
import scala.util.Try
scala> def factorial(n: Int): Try[Int] = {
| @tailrec
| def fact(i: Int, acc: Int): Int = {
| if(i <= 1) acc
| else fact(i - 1, Math.multiplyExact(i, acc))
| }
| Try(fact(n, 1))
| }
scala> factorial(12)
res0: scala.util.Try[Int] = Success(479001600)
scala> factorial(13)
res1: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: integer overflow)
关于溢出的值:你可以认为这样的结果是有效的随机的。这是只保留溢出结果的最右边 32 位(Int 类型的容量)的结果。如果设置了最高有效位,则该值将被视为负值。 (正如其他人所指出的,有关如何解释 Int 位模式的更详细说明,请参阅 Two's Complement。)虽然这取决于您正在执行的计算,但结果不会收敛 一般情况下为零。
例如,factorial(13) 是导致Int 溢出的最小参数值。如果不使用Math.multiplyExact,您的原始函数将返回值1932053504——不幸的是,这显然是错误的。如果我们使用Long 而不是Int,我们可以完全按照6227020800 计算这个结果。但是让我们看看这些数字的二进制表示:
scala> 1932053504.toBinaryString
res0: String = 1110011001010001100110000000000
scala> 6227020800L.toBinaryString
res1: String = 101110011001010001100110000000000
最右边的 32 位具有相同的值! (请注意,省略了前导零。)但是,Long 表示有一个额外的位,它不能存储在 Int 表示中,这会产生很大的不同。不幸的是,递归算法 (n! = n * (n - 1)!) 意味着一旦出现溢出,这些错误就会复合,这意味着,对于高于 13 的参数值,我们开始乘以溢出的值,从而导致总垃圾,并进一步破坏溢出的值。特别是,一旦阶乘计算为零,所有后续值显然也将为零。