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


倉庫の集計では、箱数と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言語仕様の乗算。
この掛け算自体から、オーバーフローを知らせる例外は出ません。負の結果を見つけたときは、変数宣言だけでなく、式の左右の型を確認します。数字の文字列を受け付けられないケースとは、確認する行が違います。
合計の保存先がlongかintかで、修正を選ぶ
| 許す範囲 | 実装 | 何を確認するか |
|---|---|---|
| 入力はint、合計をlongで扱える | 掛ける前に片方をlongにする | 50,000×50,000は2,500,000,000 |
| 合計をint列に保存する | Math.multiplyExact(int, int) | intに収まる値は保存でき、範囲外は拒否 |
| 入力からlong、合計もlong | Math.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,340 | 2,147,395,600 | 2,147,395,600 |
| 46,341×46,341 | 2,147,488,281 | ArithmeticException |
| 0×50、50×0 | 0 | 0 |
| −1×50、50×−1 | IllegalArgumentException | IllegalArgumentException |
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版
変数・条件分岐から、クラスの基本まで学ぶ一冊。
豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。
- Javaを基礎から体系的に学びたい
- Java 25 LTS対応の入門書を選びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
int同士の積をlongに代入しても、intであふれた結果は戻りません。大きな合計を許す仕様では掛け算の前に型を広げます。intやlongの上限を超えたら処理を止める仕様では、必要な型のMath.multiplyExactを使います。



