1. 序章

このチュートリアルでは、Javaの数値データ型のオーバーフローとアンダーフローを見ていきます。

より理論的な側面については深く掘り下げることはしません。Javaで発生する場合にのみ焦点を当てます。

最初に整数データ型を見てから、浮動小数点データ型を見ていきます。 どちらの場合も、オーバーフローまたはアンダーフローが発生したときに検出する方法についても説明します。

2. オーバーフローとアンダーフロー

簡単に言えば、変数の宣言されたデータ型の範囲外の値を割り当てると、オーバーフローとアンダーフローが発生します。

(絶対)値が大きすぎる場合はオーバーフローと呼び、値が小さすぎる場合はアンダーフローと呼びます。 

101000([X91X]1と1000ゼロ)をタイプintの変数に割り当てようとする例を見てみましょう。またはdouble。 Javaのintまたはdouble変数には値が大きすぎるため、オーバーフローが発生します。

2番目の例として、値 10-1000 (0に非常に近い)をdouble型の変数に割り当てようとしたとします。 この値は、Javaの double 変数には小さすぎるため、アンダーフローが発生します。

これらの場合のJavaで何が起こるかを詳しく見てみましょう。

3. 整数データ型

Javaの整数データタイプは、 byte (8ビット)、 short (16ビット)、 int (32ビット)、およびlongです。 ](64ビット)。

ここでは、intデータ型に焦点を当てます。 最小値と最大値が異なることを除いて、同じ動作が他のデータ型にも適用されます。

Javaのint型の整数は負または正になります。つまり、32ビットの場合、 -231 -2147483648 )との間の値を割り当てることができます。 231-1 2147483647 )。

ラッパークラスIntegerは、これらの値を保持する2つの定数Integer.MIN_VALUEInteger.MAX_VALUEを定義します。

3.1. 例

タイプintの変数mを定義し、大きすぎる値を割り当てようとするとどうなりますか(たとえば、 21474836478 = MAX_VALUE + 1)?

この割り当ての結果として、 m の値が未定義になるか、エラーが発生する可能性があります。

どちらも有効な結果です。 ただし、Javaでは、mの値は-2147483648(最小値)になります。 一方、-2147483649( = MIN_VALUE – 1 )の値を割り当てようとすると、m2147483647(最大値)になります。 この動作は整数ラップアラウンドと呼ばれます。

この動作をよりよく説明するために、次のコードスニペットを考えてみましょう。

int value = Integer.MAX_VALUE-1;
for(int i = 0; i < 4; i++, value++) {
    System.out.println(value);
}

オーバーフローを示す次の出力が得られます。

2147483646
2147483647
-2147483648
-2147483647

4. 整数データ型のアンダーフローとオーバーフローの処理

オーバーフローが発生してもJavaは例外をスローしません。 そのため、オーバーフローに起因するエラーを見つけるのは難しい場合があります。また、ほとんどのCPUで使用可能なオーバーフローフラグに直接アクセスすることもできません。

ただし、オーバーフローの可能性を処理するにはさまざまな方法があります。 これらの可能性のいくつかを見てみましょう。

4.1. 別のデータ型を使用する

2147483647 より大きい(または -2147483648 より小さい)値を許可する場合は、longデータ型またはBigInteger[ X164X]代わりに。

タイプlongの変数もオーバーフローする可能性がありますが、最小値と最大値ははるかに大きく、ほとんどの状況でおそらく十分です。

BigInteger の値の範囲は、JVMで使用可能なメモリの量を除いて制限されません。

上記の例をBigIntegerで書き直す方法を見てみましょう。

BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + "");
for(int i = 0; i < 4; i++) {
    System.out.println(largeValue);
    largeValue = largeValue.add(BigInteger.ONE);
}

次の出力が表示されます。

2147483647
2147483648
2147483649
2147483650

出力からわかるように、ここではオーバーフローはありません。 私たちの記事JavaのBigDecimalとBigIntegerは、BigIntegerをより詳細にカバーしています。

4.2. 例外をスローする

より大きな値を許可したくない場合や、オーバーフローを発生させたくない場合があり、代わりに例外をスローしたい場合があります。

Java 8の時点で、正確な算術演算のメソッドを使用できます。最初に例を見てみましょう。

int value = Integer.MAX_VALUE-1;
for(int i = 0; i < 4; i++) {
    System.out.println(value);
    value = Math.addExact(value, 1);
}

静的メソッドaddExact()は通常の加算を実行しますが、操作によってオーバーフローまたはアンダーフローが発生した場合は例外をスローします。

2147483646
2147483647
Exception in thread "main" java.lang.ArithmeticException: integer overflow
	at java.lang.Math.addExact(Math.java:790)
	at baeldung.underoverflow.OverUnderflow.main(OverUnderflow.java:115)

addExact()に加えて、Java8のMath パッケージは、すべての算術演算に対応する正確なメソッドを提供します。 これらすべてのメソッドのリストについては、Javaドキュメントを参照してください。

さらに、別のデータ型への変換中にオーバーフローが発生した場合に例外をスローする正確な変換メソッドがあります。

longからintへの変換の場合:

public static int toIntExact(long a)

また、BigIntegerからintまたはlongへの変換の場合:

BigInteger largeValue = BigInteger.TEN;
long longValue = largeValue.longValueExact();
int intValue = largeValue.intValueExact();

4.3. Java8より前

正確な算術メソッドがJava8に追加されました。 以前のバージョンを使用する場合は、これらのメソッドを自分で作成するだけです。 そのための1つのオプションは、Java8と同じメソッドを実装することです。

public static int addExact(int x, int y) {
    int r = x + y;
    if (((x ^ r) & (y ^ r)) < 0) {
        throw new ArithmeticException("int overflow");
    }
    return r;
}

5. 非整数データ型

非整数型floatおよびdoubleは、算術演算に関しては整数データ型と同じように動作しません。

1つの違いは、浮動小数点数の算術演算でNaNが発生する可能性があることです。 JavaにはNaNに関する専用の記事があるため、この記事ではこれ以上詳しく説明しません。 さらに、Mathパッケージの非整数型にはaddExactmultiplyExactなどの正確な算術演算方法はありません。

Javaは、floatおよびdoubleデータ型について、 IEEE Standard for Floating-Point Arithmetic(IEEE 754)に準拠しています。 この標準は、Javaが浮動小数点数のオーバーフローとアンダーフローを処理する方法の基礎です。

以下のセクションでは、 double データ型のオーバーフローとアンダーフロー、およびそれらが発生する状況を処理するために実行できることに焦点を当てます。

5.1. オーバーフロー

整数データ型に関しては、次のことが予想されます。

assertTrue(Double.MAX_VALUE + 1 == Double.MIN_VALUE);

ただし、浮動小数点変数の場合はそうではありません。 次のことが当てはまります。

assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);

これは、double値の重要ビットの数が限られているためです。 大きなdouble値の値を1つだけ増やす場合、重要なビットは変更されません。 したがって、値は同じままです。

変数の有効ビットの1つを増やすように変数の値を増やすと、変数の値はINFINITYになります。

assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

負の値の場合はNEGATIVE_INFINITY

assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

整数とは異なり、ラップアラウンドはありませんが、オーバーフローの2つの異なる結果が考えられます。値が同じままであるか、特別な値の1つPOSTIVE_INFINITYまたはNEGATIVE_INFINITY[]を取得します。 X228X]。

5.2. アンダーフロー

double 値の最小値には、 MIN_VALUE (4.9e-324)と MIN_NORMAL (2.2250738585072014E-308)の2つの定数が定義されています。

浮動小数点演算のIEEE標準(IEEE 754)では、これらの違いの詳細について詳しく説明しています。

浮動小数点数の最小値が必要な理由に焦点を当てましょう。

double の値は、値を表すビット数が限られているため、任意に小さくすることはできません。

Java SE言語仕様のタイプ、値、および変数に関する章では、浮動小数点型の表現方法について説明しています。 double のバイナリ表現の最小指数は、-1074として与えられます。 つまり、doubleが持つことができる最小の正の値は、 Math.pow(2、-1074)であり、これは4.9e-324に等しくなります。

結果として、Javaの double の精度は、0と 4.9e-324の間、の間、または-4.9e-324と[の間の値をサポートしません。 X151X]0負の値の場合。

では、タイプ double の変数に小さすぎる値を割り当てようとすると、どうなるでしょうか。 例を見てみましょう:

for(int i = 1073; i <= 1076; i++) {
    System.out.println("2^" + i + " = " + Math.pow(2, -i));
}

出力あり:

2^1073 = 1.0E-323
2^1074 = 4.9E-324
2^1075 = 0.0
2^1076 = 0.0

小さすぎる値を割り当てると、アンダーフローが発生し、結果の値は 0.0 (正のゼロ)であることがわかります。 同様に、負の値の場合、アンダーフローは -0.0 (負のゼロ)の値になります。

6. 浮動小数点データ型のアンダーフローとオーバーフローの検出

オーバーフローは正または負の無限大になり、アンダーフローは正または負のゼロになるため、整数データ型のような正確な算術メソッドは必要ありません。代わりに、これらの特別な定数をチェックできます。オーバーフローとアンダーフローを検出します。

この状況で例外をスローしたい場合は、ヘルパーメソッドを実装できます。 それがべき乗をどのように探すことができるかを見てみましょう:

public static double powExact(double base, double exponent) {
    if(base == 0.0) {
        return 0.0;
    }
    
    double result = Math.pow(base, exponent);
    
    if(result == Double.POSITIVE_INFINITY ) {
        throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY");
    } else if(result == Double.NEGATIVE_INFINITY) {
        throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY");
    } else if(Double.compare(-0.0f, result) == 0) {
        throw new ArithmeticException("Double overflow resulting in negative zero");
    } else if(Double.compare(+0.0f, result) == 0) {
        throw new ArithmeticException("Double overflow resulting in positive zero");
    }

    return result;
}

このメソッドでは、メソッド Double.compare()を使用する必要があります。 通常の比較演算子( < >> )正と負のゼロを区別しないでください。

7. ポジティブとネガティブゼロ

最後に、正と負のゼロと無限大を操作するときに注意が必要な理由を示す例を見てみましょう。

示すために、いくつかの変数を定義しましょう。

double a = +0f;
double b = -0f;

正と負の0は等しいと見なされるため、次のようになります。

assertTrue(a == b);

正の無限大と負の無限大は異なると見なされますが、次のようになります。

assertTrue(1/a == Double.POSITIVE_INFINITY);
assertTrue(1/b == Double.NEGATIVE_INFINITY);

ただし、次のアサーションは正しいです。

assertTrue(1/a != 1/b);

これは私たちの最初の主張と矛盾しているようです。

8. 結論

この記事では、オーバーフローとアンダーフロー、Javaでの発生方法、整数データ型と浮動小数点データ型の違いについて説明しました。

また、プログラムの実行中にオーバーフローとアンダーフローを検出する方法も確認しました。

いつものように、完全なソースコードはGithubから入手できます。