小さなトレーから積み木があふれ、その先に大きなトレーがあるイラスト

実装・エラー解決

Javaのintの掛け算はlongに代入してもあふれる|原因と直し方

この記事でわかること

int同士の掛け算をlongに代入したら、在庫数がマイナスになりました。どの時点で数があふれたかを確認し、合計をlongで扱う修正と、intの範囲外を例外で止める修正を比べます。

新人SE
新人SE
集計の変数をlongにしたのに、50,000×50,000がマイナスになりました。どこで値が変わったんですか?
ポンコツSE
ポンコツSE
掛ける値が両方intなら、掛け算の時点でintの範囲を使います。その結果が、後からlongに入ります。

倉庫の集計では、箱数と1箱あたりの個数を受け取って合計個数を出します。個々の値はintで受け取れますが、掛け合わせると約21億を超えることがあります。たとえば50_000 * 50_000の答えは2,500,000,000。受け皿だけlongにしたコードでは、実行すると −1,794,967,296 になります。

この練習では、箱数も1箱あたりの個数も0以上の整数です。0は合計0件として受け付け、負数は入力として拒否します。保存先がlongを受け付けるか、intのままかによって修正が変わります。

実行サンプル

あふれるコードと修正版、9件のJUnit、pom.xml、READMEを収録。JDK 17とMavenで、展開したディレクトリから mvn clean test を実行できます。

longへ代入する前に、intの掛け算が終わっている

次は、負の合計を再現するBrokenInventoryTotal.javaの全文です。

package example.inventory;

final class BrokenInventoryTotal {
    static long total(int boxes, int itemsPerBox) {
        long result = boxes * itemsPerBox;
        return result;
    }
}

boxesとitemsPerBoxが両方intなので、式boxes * itemsPerBoxの結果もintです。この演算の範囲は −2,147,483,648 から 2,147,483,647。今回の答えは上限を超え、32ビットに収まる結果が −1,794,967,296 になります。その値をlongへ代入しても、元の2,500,000,000には戻りません。(long) (boxes * itemsPerBox)と外側を囲んでも、掛けた後に変換する順序は同じです。Java言語仕様の乗算。

この掛け算自体から、オーバーフローを知らせる例外は出ません。負の結果を見つけたときは、変数宣言だけでなく、式の左右の型を確認します。数字の文字列を受け付けられないケースとは、確認する行が違います。

関連記事JavaのNumberFormatExceptionの原因と対処|空文字・カンマ・桁あふれ 文字列を整数に変える時点で失敗する場合は、空文字・区切り文字・入力の範囲を確かめます。 記事を読む

合計の保存先がlongかintかで、修正を選ぶ

許す範囲実装何を確認するか
入力はint、合計をlongで扱える掛ける前に片方をlongにする50,000×50,000は2,500,000,000
合計をint列に保存するMath.multiplyExact(int, int)intに収まる値は保存でき、範囲外は拒否
入力からlong、合計もlongMath.multiplyExact(long, long)Long.MAX_VALUE×2は拒否

次は、修正版InventoryTotal.javaの全文です。どのメソッドも、先に入力が負数でないことを確認します。計算が型に収まるかと、入力として受け付けるかは別の条件です。

package example.inventory;

final class InventoryTotal {
    static long total(int boxes, int itemsPerBox) {
        requireNonNegative(boxes, itemsPerBox);
        return (long) boxes * itemsPerBox;
    }

    static int totalForIntColumn(int boxes, int itemsPerBox) {
        requireNonNegative(boxes, itemsPerBox);
        return Math.multiplyExact(boxes, itemsPerBox);
    }

    static long totalFromLongCounts(long boxes, long itemsPerBox) {
        requireNonNegative(boxes, itemsPerBox);
        return Math.multiplyExact(boxes, itemsPerBox);
    }

    private static void requireNonNegative(long boxes, long itemsPerBox) {
        if (boxes < 0 || itemsPerBox < 0) {
            throw new IllegalArgumentException("箱数と1箱あたりの個数は0以上です");
        }
    }
}

totalでは(long) boxes * itemsPerBoxと、左の値を掛ける前に広げます。右もlongとして計算されます。この例では入力が二つとも非負のintなので、最大同士を掛けてもlongに収まります。APIや保存先がlongの合計を受け付ける場合に使えます。

totalForIntColumnでは、保存先の最大値を超える合計をArithmeticExceptionで拒否します。Math.multiplyExact(boxes, itemsPerBox)の両引数がintなら、戻り値もintの版が選ばれます。代入先だけlongにしても、選ばれるメソッドは同じです。すでに計算済みのlongをintへ狭める場面なら、Math.toIntExactでその変換を検査できます。Java 17のMath API。

totalFromLongCountsは、入力の箱数や個数もlongになる別の仕様です。両引数をlongにしてMath.multiplyExactを呼び、longの範囲を超える積を拒否します。通常のlongの掛け算でも、範囲外の積は自動で止まりません。

intの上限をまたぐ値と、負の入力を別に試す

配布ZIPを展開し、pom.xmlがある場所でmvn clean testを実行します。InventoryTotalTest.javaは、壊れた例の出力と修正版の結果を同じ50,000×50,000で比べたあと、境界と入力仕様を確認します。

入力合計をlongで返す合計をintで返す
46,340×46,3402,147,395,6002,147,395,600
46,341×46,3412,147,488,281ArithmeticException
0×50、50×000
−1×50、50×−1IllegalArgumentExceptionIllegalArgumentException
InventoryTotalTest.javaの全文を見る
package example.inventory;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class InventoryTotalTest {
    @Test
    void assigningToLongAfterMultiplicationDoesNotRecoverLostBits() {
        assertEquals(-1_794_967_296L, BrokenInventoryTotal.total(50_000, 50_000));
        assertEquals(2_500_000_000L, InventoryTotal.total(50_000, 50_000));
    }

    @Test
    void valuesAroundTheIntBoundaryKeepTheirLongProduct() {
        assertEquals(2_147_395_600L, InventoryTotal.total(46_340, 46_340));
        assertEquals(2_147_488_281L, InventoryTotal.total(46_341, 46_341));
    }

    @Test
    void largestPositiveIntInputsStillFitInLong() {
        assertEquals(4_611_686_014_132_420_609L,
                InventoryTotal.total(Integer.MAX_VALUE, Integer.MAX_VALUE));
    }

    @Test
    void intColumnAcceptsTheMaximumAndRejectsOverflow() {
        assertEquals(Integer.MAX_VALUE, InventoryTotal.totalForIntColumn(Integer.MAX_VALUE, 1));
        assertEquals(2_147_395_600, InventoryTotal.totalForIntColumn(46_340, 46_340));
        assertThrows(ArithmeticException.class, () -> InventoryTotal.totalForIntColumn(46_341, 46_341));
    }

    @Test
    void longInputsAlsoHaveALimit() {
        assertEquals(Long.MAX_VALUE, InventoryTotal.totalFromLongCounts(Long.MAX_VALUE, 1));
        assertThrows(ArithmeticException.class, () -> InventoryTotal.totalFromLongCounts(Long.MAX_VALUE, 2));
    }

    @ParameterizedTest
    @CsvSource({"0, 50", "50, 0"})
    void acceptsZero(int boxes, int itemsPerBox) {
        assertEquals(0L, InventoryTotal.total(boxes, itemsPerBox));
        assertEquals(0, InventoryTotal.totalForIntColumn(boxes, itemsPerBox));
        assertEquals(0L, InventoryTotal.totalFromLongCounts(boxes, itemsPerBox));
    }

    @ParameterizedTest
    @CsvSource({"-1, 50", "50, -1"})
    void rejectsNegativeInputEvenWhenTheProductFits(int boxes, int itemsPerBox) {
        assertThrows(IllegalArgumentException.class, () -> InventoryTotal.total(boxes, itemsPerBox));
        assertThrows(IllegalArgumentException.class, () -> InventoryTotal.totalForIntColumn(boxes, itemsPerBox));
        assertThrows(IllegalArgumentException.class, () -> InventoryTotal.totalFromLongCounts(boxes, itemsPerBox));
    }
}
pom.xmlの全文を見る
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>java-int-multiplication-overflow</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.12.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.1</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.3</version>
      </plugin>
    </plugins>
  </build>
</project>

通常は9件が成功します。修正版totalから(long)を外すと、50,000の再現、上限をまたぐ掛け算、最大同士の積の3件が失敗します。上限内の値だけでテストすると、掛ける前に広げる変更が消えても分からないため、範囲をまたぐ値を残します。

レビュー前に、掛け算と保存先の型を確認する

レビューでは「合計をlongへ入れていますが、掛け算の左右はintです。合計をそのまま扱えるなら片方を演算前に広げ、保存列がintなら範囲外を拒否するケースを追加できますか?」と確認します。両方の層をまたいで値を持ち運ぶ場合は、Javaの戻り値だけでなく、後続のAPIやDB列の型まで読みます。

  • 掛け算の左右がどの型か確認した。
  • 合計がintを超えてよいか、保存先まで確認した。
  • intで許す上限と、その上をテストした。
  • 負数の入力拒否と、積のオーバーフローを別々に確認した。

型と式の違いを、ほかの計算でも練習するなら

式の途中で値がどの型になるかを追う練習に戻るなら『スッキリわかるJava入門 第5版』が候補です。今回の例では「代入先の型」と「演算で使う型」を分けました。次は自分の集計コードで、入力、途中の式、保存先の3か所に型を書き出すと、どこで値の範囲が変わるかを説明しやすくなります。

書籍「スッキリわかるJava入門 第5版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

変数・条件分岐から、クラスの基本まで学ぶ一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

関連記事Java初心者におすすめの最初の1冊|Spring Bootに進むまでの学び方 式や変数の型から復習したいとき、最初の1冊と次へ進む目安を確認できます。 記事を読む 関連記事BigDecimal.divideでArithmeticExceptionになる原因|割り切れない計算の丸め方 小数の割り算で例外が出た場合に、何桁を残してどの方向に丸めるかを決めます。 記事を読む

まとめ

int同士の積をlongに代入しても、intであふれた結果は戻りません。大きな合計を許す仕様では掛け算の前に型を広げます。intやlongの上限を超えたら処理を止める仕様では、必要な型のMath.multiplyExactを使います。

-実装・エラー解決
-, ,