JavaでNumberFormatExceptionになる空文字やカンマ、全角数字の対処を解説する記事のアイキャッチ。

実装・エラー解決

JavaのNumberFormatExceptionの原因と対処|空文字・カンマ・桁あふれ

この記事でわかること

Integer.parseIntのNumberFormatExceptionは、未入力、整数として読めない文字、intの範囲外を分けて調べます。全角数字「123」はJava 17ではそのまま123に変換できます。この記事では、3桁カンマを許可する数量欄を例に、形式を確認してから変換し、必須・形式・範囲のエラーを分けて返すコードを示します。

新人SE
新人SE
画面では数字に見えるのに、Integer.parseIntでNumberFormatExceptionになりました。
ポンコツSE
ポンコツSE
空白、カンマ、小数点と、intの範囲外を先に確認しましょう。全角数字だけなら変換できるので、「全角だから」と決めつけないことが大切です。

画面やCSVから受け取った数量をInteger.parseIntへ渡して失敗したときは、「受け取った文字列」「変換する型」「許可する入力形式」を分けて確認します。数字以外の文字が原因の場合と、数字だけでもintに収まらない場合では、直す箇所が違います。

よくある修正はtry-catchして0を返すことですが、未入力、形式不正、値0を同じにしてしまいます。どの文字列を許可し、どのエラーとして返すかを入力境界で決めます。

この記事のポイント

  • 空文字や空白はparseIntできない
  • カンマ・単位付きはparseIntできないが、全角数字は変換できる
  • int範囲外でもNumberFormatExceptionになる
  • 不正値を0へ置き換えて隠さない
  • 画面・CSV・APIごとに入力契約とエラーを決める

数量欄の正常値と3種類の入力エラーを試す

記事に載せた QuantityParser.java と QuantityParserTest.java、実行設定の pom.xml を収録しています。JDK 17とMaven 3.9系を用意し、ZIPを展開したフォルダで mvn test を実行します。

まず入力値と変換先の型を確認する

次はJava 17のInteger.parseInt(String)の結果です。各行は独立した例です。途中で例外になる行があるため、そのまま上から続けて実行すると最初の例外で止まります。

Integer.parseInt("");           // NumberFormatException: 空文字
Integer.parseInt(" 123 ");      // NumberFormatException: 前後の空白
Integer.parseInt("1,234");      // NumberFormatException: カンマ
Integer.parseInt("12.5");       // NumberFormatException: 小数点
Integer.parseInt("2147483648"); // NumberFormatException: intの上限超過
Integer.parseInt("123");      // 123: 全角数字は変換できる

まず例外が発生した自分のコードの行を開き、変換直前の値を確認します。値がnullまたは空なら、フォーム名・CSV列・環境変数など取得元を調べます。値があるなら空白・カンマ・単位・小数点を確認し、数字だけなら変換先の上限と照合します。Integer.parseInt((String) null)もNumberFormatExceptionになります。

例えばCSVの数量欄が"1,234"だった場合、必要なのは「数字かどうか」という曖昧なチェックではなく、「カンマ付き数量を受け付ける仕様か」の確認です。仕様で禁止しているなら入力エラーにし、許可しているなら3桁区切りを検証してから変換します。

例外メッセージに入力値が含まれることがあります。本番データをそのままログへ追加するのではなく、再現用の値で空白や記号の違いを確認します。

空文字と空白を先に扱う

任意入力なら、未入力をnullとして扱うのか既定値にするのかを決めてから変換します。次の例は、未入力はnull、前後の空白はstrip()で取り除く仕様です。必須入力や、空白を認めないCSVの仕様へそのまま流用しないでください。

Integer parseOptionalInt(String value) {
    if (value == null || value.isBlank()) {
        return null;
    }
    return Integer.valueOf(value.strip());
}

戻り値nullをServiceへ広げると別のNPEを生むことがあります。Formでは文字列、変換後DTOではIntegerなど、境界と内部型を分ける設計も検討します。

カンマは3桁区切りを検証してから取り除く

1,234はInteger.parseIntでは変換できません。ただし、無条件のreplace(",", "")では12,34まで1234になります。先に文字列全体の形式を検証します。

NumberFormatに置き換えるだけでも不十分です。Java 17の日本ロケールでは、次のように不正な区切り12,34を最後まで読んで1234にします。

import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;

NumberFormat format = NumberFormat.getIntegerInstance(Locale.JAPAN);
ParsePosition position = new ParsePosition(0);
Number number = format.parse("12,34", position);

System.out.println(number);              // 1234
System.out.println(position.getIndex()); // 5: 最後まで読んでも区切りは不正

ParsePositionは「1,234円の円を読まずに終わった」といった途中終了の検出には使えます。しかし、全体を読んだことと3桁区切りが正しいことは別です。setParseIntegerOnly(true)も桁区切りの検証を追加する設定ではありません。

次は、数量欄で「半角数字、カンマなし、または3桁区切りのカンマ」を許可する例です。0〜2,147,483,647を受け付け、前後空白・符号・小数・単位・全角数字は入力エラーにします。先頭の0は許可しています。

public final class QuantityParser {
    private QuantityParser() {}

    public static int parse(String input) {
        if (input == null || input.isBlank()) {
            throw new IllegalArgumentException("数量を入力してください");
        }
        // ASCII数字のみ。カンマを使うなら先頭1〜3桁、以降は3桁ずつ。
        if (!input.matches("(?:[0-9]+|[0-9]{1,3}(?:,[0-9]{3})+)")) {
            throw new IllegalArgumentException("数量は半角数字と3桁区切りのカンマで入力してください");
        }
        try {
            return Integer.parseInt(input.replace(",", ""));
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("数量は0〜2,147,483,647で入力してください", e);
        }
    }
}

matchesは文字列全体を照合します。[0-9]+はカンマなし、後半は先頭1〜3桁と「カンマ+3桁」の繰り返しです。1,234と1234は通り、12,34や1,23,456は変換前に止まります。形式が正しくてもintに収まらなければ、最後のcatchで範囲エラーにします。

この正規表現は上記の数量欄の仕様に限定したものです。小数や負数を扱う金額欄、桁区切りが異なるロケールへ共通化しないでください。既存の入力部品がある場合も、12,34を拒否するか、その部品のテストで確かめます。

全角数字はparseIntできる。半角限定なら別に検証する

Integer.parseInt("123")はJava 17で123になります。parseIntはASCIIの0〜9だけを受け付けるメソッドではなく、各桁をCharacter.digitの規則で判定します。「全角数字だからNumberFormatExceptionになる」という切り分けは誤りです。

Integer.parseInt("123");   // 123
Integer.parseInt("-123");  // -123: 符号はASCIIのマイナス
Integer.parseInt("-123"); // NumberFormatException: 全角のマイナス
Integer.parseInt(" 123"); // NumberFormatException: 先頭の全角空白

画面で似て見えても、数字と符号・空白は別です。全角入力で失敗したときは、数字以外に全角のマイナスや空白が付いていないかも確認します。

前節のQuantityParserが123を拒否するのは、parseIntの制約ではなく「半角数字のみ」という入力仕様を[0-9]で検証しているためです。APIで変換できる値と、業務で許可する値を混同しないでください。

全角数字を受け付けるだけなら、parseIntのためにNFKC正規化を足す必要はありません。Normalizer.normalize("①", Normalizer.Form.NFKC)は"1"にも変わります。全角数字だけを許可したつもりで、丸数字など別の文字まで受け付けないよう、正規化の前後と許可範囲を別々に決めます。

桁あふれでもNumberFormatException

文字がすべて数字でも、intの範囲を超えると変換できません。IDや金額が将来大きくなる場合、型選択を確認します。

Integer.parseInt("2147483647"); // 最大値
Integer.parseInt("2147483648"); // NumberFormatException

long id = Long.parseLong("2147483648");

longでも足りない整数、または桁数を失いたくないコード値ならBigIntegerやStringを検討します。電話番号、郵便番号、商品コードは計算しないため、数字だけでも数値型が適切とは限りません。

Integer.parseInt("2147483648")は変換の時点で例外になります。50,000を数値へ変換してからint同士で掛け算した結果がマイナスになる場合は、変換は成功しているので掛け算を行う式の型を確認します。

関連記事Javaのintの掛け算はlongに代入してもあふれる|原因と直し方 個数の変換には成功したのに、掛け算でマイナスになった場合はこちら。計算前に型を広げる修正を、同じ入力で動かせます。 記事を読む

小数と金額は適切な型へ変換する

小数をIntegerへ変換するのではなく、用途に合う型を使います。金額や率ではBigDecimalを文字列から作ると、double経由の誤差を避けられます。

BigDecimal amount = new BigDecimal("1234.50");

// 避けたい例
BigDecimal inaccurate = new BigDecimal(0.1);

BigDecimalでもカンマや通貨記号はそのまま受け付けません。入力フォーマットの解析と、内部の数値表現を分けます。

必須・形式・範囲のエラーを分けて返す

前節のQuantityParserは、未入力なら「数量を入力してください」、12,34なら形式エラー、2,147,483,648なら範囲エラーを返します。利用者は空欄を埋める、カンマを直す、上限以下にする、のどれが必要か判断できます。すべてを「整数で入力してください」にすると、数字だけの上限超過でも原因が伝わりません。

例では入力不正を標準のIllegalArgumentExceptionで表しています。画面やCSVへ組み込む際は、QuantityParserを呼ぶ境界でこの入力エラーを該当する数量欄・CSV行のエラーへ対応付けます。このクラスを置くだけで、HTTPの400や画面の項目エラーへ自動変換されるわけではありません。

「注文数は1〜999まで」の業務ルールが別にあるなら、数値へ変換した後にその範囲も確認します。intに収まることは、注文できる数量であることを保証しません。エラーをcatchして0を返すと、未入力や書式不正が数量0として処理されるため避けます。

JUnitで「通す値」と「拒否する値」を両方確認する

カンマの節に載せたQuantityParser.parseを呼び、変換値とエラーの種類を確かめます。123は半角限定の規則、12,34は3桁区切りの規則で拒否します。2,147,483,648は形式を通るので、上限超過のメッセージと、原因として残したNumberFormatExceptionを確認します。以下はZIPに収録したQuantityParserTest.javaの全文です。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class QuantityParserTest {
    @ParameterizedTest
    @CsvSource(value = {"0|0", "1234|1234", "1,234|1234", "0001|1", "000,001|1",
            "2147483647|2147483647", "2,147,483,647|2147483647"}, delimiter = '|')
    void 許可した形式を数値に変換する(String input, int expected) {
        assertEquals(expected, QuantityParser.parse(input));
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {" ", " "})
    void 未入力には必須のエラーを返す(String input) {
        var error = assertThrowsExactly(IllegalArgumentException.class,
                () -> QuantityParser.parse(input));
        assertEquals("数量を入力してください", error.getMessage());
        assertNull(error.getCause());
    }

    @ParameterizedTest
    @ValueSource(strings = {" 123 ", "12,34", "1,23,456", "1234,567", "1,,234",
            "123", "1.5", "-1", "+1"})
    void 許可しない書式には形式のエラーを返す(String input) {
        var error = assertThrowsExactly(IllegalArgumentException.class,
                () -> QuantityParser.parse(input));
        assertEquals("数量は半角数字と3桁区切りのカンマで入力してください", error.getMessage());
        assertNull(error.getCause());
    }

    @ParameterizedTest
    @ValueSource(strings = {"2147483648", "2,147,483,648", "999999999999999999999"})
    void int上限超過には範囲のエラーと原因を返す(String input) {
        var error = assertThrowsExactly(IllegalArgumentException.class,
                () -> QuantityParser.parse(input));
        assertEquals("数量は0〜2,147,483,647で入力してください", error.getMessage());
        assertInstanceOf(NumberFormatException.class, error.getCause());
    }
}

上限ちょうどの2,147,483,647を通し、1大きい2,147,483,648を拒否する組で確認します。カンマも正常な1,234と不正な12,34を対にすると、単なるカンマ除去へ戻した変更を検出できます。

NumberFormatExceptionはIllegalArgumentExceptionのサブクラスです。assertThrows(IllegalArgumentException.class, ...)だけでは、範囲エラーを作るcatchを外しても検出できません。展開したZIPでmvn testを実行すると23件が通ります。catchを外すと、assertThrowsExactlyでアプリ側の例外型を指定した範囲超過の3件だけが失敗します。JUnitのassertThrowsExactlyの仕様

レビューでは許可形式とテスト結果を確認する

数値変換のレビューではcatchの有無ではなく、入力形式、正規化、型の範囲、不正時の利用者向けエラーが一貫しているかを見ます。

// レビューコメント例
NumberFormatExceptionをcatchして0にすると、不正入力と数量0を区別できません。
数量欄のエラーとして返してください。

// レビューコメント例
カンマをreplaceするだけでも、NumberFormatで最後まで読んだだけでも12,34を通します。
3桁区切りを先に検証し、1,234が成功、12,34が失敗するテストを追加してください。

// レビューコメント例
この値は商品コードで計算しません。先頭0も必要なので、
IntegerではなくStringのまま扱う方が契約に合います。

レビューへの返信では、「正規表現を足しました」だけで終えず、許可した入力と拒否する入力を答えます。例えば「1,234は1234へ変換し、12,34と上限超過は別のエラーにしました。境界値もテスト済みです」と説明できれば、実装が仕様に沿っているか判断できます。

提出前のセルフチェック

レビュー前に確認すること

  • null・空文字・空白の扱いを決めたか
  • 1,234を許可し、12,34を拒否できるか
  • 全角数字を受け付けるか、半角限定で検証するか決めたか
  • 正規化が他の文字を変えないか
  • 小数を整数型へ入れようとしていないか
  • intやlongの範囲を超えないか
  • コード値を数値型にする必要があるか
  • 不正値を0で隠していないか
  • 項目エラーとして利用者へ返せるか

Javaの型と例外を基本から確認する参考書

文字列、数値型、例外処理を一緒に整理すると、変換失敗をcatchで隠さず、入力境界で正しく扱えるようになります。

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

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

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

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

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

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

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

この記事とあわせて読みたい

関連記事BigDecimal.divideでArithmeticExceptionになる原因|割り切れない計算の丸め方 数値へ変換した後の割り算と端数処理を解説します。 記事を読む 関連記事Spring Bootで400 Bad Requestになる原因|Controllerに値が入らない時の確認順 Controllerの型変換失敗を含む400エラーの確認順を整理します。 記事を読む 関連記事Javaのスタックトレースの読み方|Caused byの後、どのコードを開けばいい? 例外名だけでなく、causeと呼び出し位置を調査する方法を確認します。 記事を読む

まとめ

Integer.parseIntのNumberFormatExceptionは、未入力、整数として読めない文字、intの範囲外を分けて調べます。全角数字そのものは変換できるため、全角の符号や空白、半角限定の独自チェックと混同しないでください。

カンマを許可する場合は3桁区切りを検証してから取り除きます。NumberFormatの全消費チェックだけでは不正な区切りを見逃します。必須・形式・範囲のエラーを分け、正常な1,234、不正な12,34、intの上限と上限超過をセットで確認してください。

-実装・エラー解決
-,