Javaにおける整数オーバーフロー
Java言語仕様§4.2.2「整数演算」には次のように書かれています。
いかなる場合であっても、組み込みの整数演算子がオーバーフローやアンダーフローを起こすことはない。整数演算は、null参照のアンボクシング変換が要求された場合、NullPointerExceptionをスローすることができる。それ以外の場合、例外をスローできる演算子は、右手側オペランドがゼロである場合にArithmeticExceptionをスローする整数除算演算子/と整数剰余演算子%、および、ボクシング変換を必要とするものの、変換を実行するための十分なメモリが利用可能でない場合にOutOfMemoryErrorをスローするインクリメントおよびデクリメント演算子++と--のみである。
また、§15.17.1「乗算演算子*」には次のように書かれています。
整数演算がオーバーフローした場合、結果は十分な大きさの2の補数表現によって表現された数学的な積の下位ビットとなる。
つまり、プログラマ自身が整数オーバーフローの発生を検知するようなコードを書くか、あるいはそもそも整数オーバーフローを引き起こさないようなコードを書かない限り、オーバーフローが発生してもプログラムは黙って実行を続けるということです。オーバーフローの発生は、計算間違いやプログラムの想定外の動作を引き起こす可能性があるため、避けなければいけません。整数オーバーフローが発生する可能性があるところでは、明示的に整数オーバーフローをチェックすべきです。ではどのように書けばよいか、次に見ていきましょう。
整数オーバーフローを検出・防止する3つの手法
整数オーバーフローの検出には大きく3つのアプローチが考えられます。
アプローチ1. 事前条件テスト
事前条件テストでは、演算子に渡すオペランドの値が整数オーバーフローを発生させない範囲に収まっていることをまず確認します。整数オーバーフローが発生する場合にはArithmeticExceptionをスローし、そうでない場合にだけ演算を行います。例えばint型同士のかけ算を安全に行うための事前条件テストは以下のように書くことができます。
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; }
事前条件テストを行うコードは非常に煩雑になるため、一般にアップキャストを行う方がコードを簡潔に書けるでしょう。
アプローチ2. アップキャスト
アップキャストでは演算をより大きな型で行い、オーバーフローの発生を防止します。以下にアップキャストを行うコード例を示します。
public static long intRangeCheck(long value) throws ArithmeticException { if ((value < Integer.MIN_VALUE) || (value > Integer.MAX_VALUE)) { throw new ArithmeticException("Integer overflow"); } return value; } public static int multAccum(int oldAcc, int newVal, int scale) throws ArithmeticException { final long res = intRangeCheck(((long) oldAcc) + intRangeCheck((long) newVal * (long) scale)); return (int) res; // safe down-cast }
行いたい算術演算は、oldAcc+newVal×scaleです。これを正しくアップキャストし、演算結果の範囲をチェックするために、intRangeCheck()メソッドを部分式ごとに適用しています。まず newVal×scaleの演算をlong型で行い、その結果の値がint型で表現できる範囲に収まることを確認します。次にoldAccを加算した結果がint型で表現できる範囲に収まることを確認しています。ここまで例外がスローされずに実行できれば、演算結果を安全にダウンキャストし、intの値としてreturnすることができます。
ただし、long 型はJavaのプリミティブ整数型のうち最も大きな型なので、long型の演算はそれ以上アップキャストすることはできません。そのような場合には、アプローチ3で紹介するBigIntegerを使う必要があります。
アプローチ3. BigInteger の使用
オペランドをBigIntegerクラスのオブジェクトに変換し、すべての算術演算をBigIntegerクラスが提供するメソッドを使って行います。BigIntegerを使った演算は整数オーバーフローしないことが保証されており、long型同士あるいはより大きな値の演算する場合に有効です。BigIntegerを使ってオーバーフローを検出するコードは以下のように書くことができます。
private static final BigInteger bigMaxInt = BigInteger.valueOf(Integer.MAX_VALUE); private static final BigInteger bigMinInt = BigInteger.valueOf(Integer.MIN_VALUE); public static BigInteger intRangeCheck(BigInteger val) throws ArithmeticException { if (val.compareTo(bigMaxInt) == 1 || val.compareTo(bigMinInt) == -1) { throw new ArithmeticException("Integer overflow"); } return val; } public static int multAccum(int oldAcc, int newVal, int scale) throws ArithmeticException { BigInteger product = BigInteger.valueOf(newVal).multiply(BigInteger.valueOf(scale)); BigInteger res = intRangeCheck(BigInteger.valueOf(oldAcc).add(product)); return res.intValue(); // safe conversion }
それでは次のページで、実際のAndroidアプリケーションで整数オーバーフローが問題(プログラムが起動できない)につながった例を見てみましょう。