IllegalArgumentExceptionとは、メソッドに渡された引数の値が受け付けられないときに投げる例外です。 よく比べられる IllegalStateException は、引数ではなく呼ぶタイミング(オブジェクトの状態)が合わないときに投げます。「引数を変えれば通るか」と考えると、ほとんどの場面で迷わず選べます。

この記事では、NullPointerException・IndexOutOfBoundsException・UnsupportedOperationException も含めた判断表を先に示し、標準APIが実際に投げる例をまとめて実行した結果を載せました。後半には、Objects.requireNonNull、Objects.checkIndex と、自作の引数チェック用メソッドを、そのまま貼って使える形で用意しています。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(javac でコンパイルし、java コマンドで実行)。

例外の使い分け早見表

何が合わないのか 投げる例外 例 標準APIの実例
引数の値(範囲・形式・組み合わせ) IllegalArgumentException 個数に負の数、空の名前 new ArrayList<>(-1)
引数が null NullPointerException 必須の引数に null Objects.requireNonNull(null, "name")
引数の位置(インデックス)が範囲外 IndexOutOfBoundsException 3件のリストの5番目 Objects.checkIndex(5, 3)
呼ぶタイミング・状態 IllegalStateException 開始前に終了を呼ぶ、2回閉じる Iterator.remove() を続けて2回
その操作を用意していない UnsupportedOperationException 変更できないリストに追加 List.of(1).add(2)
取り出す要素がない NoSuchElementException 空の入れ物から取り出す Optional.empty().orElseThrow()
数値にできない文字列 NumberFormatException(IllegalArgumentException の子) "12a" を整数に Integer.parseInt("12a")

迷ったときの順番は次のとおりです。

  1. 引数が null か → NullPointerException(Objects.requireNonNull)
  2. 引数が位置(インデックス)で、範囲の外か → IndexOutOfBoundsException(Objects.checkIndex)
  3. 引数の値を変えれば通るか → 通るなら IllegalArgumentException
  4. どんな引数でも、今の状態では通らないか → IllegalStateException
  5. このクラスではそもそも用意していない操作か → UnsupportedOperationException

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java IllegalArgumentExceptionの使い分け早見表」

IllegalArgumentException と IllegalStateException の違い

Java 21 の API ドキュメントでは、2つは次のように説明されています(出典は記事末尾)。

例外 API ドキュメントの説明(要旨) 親クラス
IllegalArgumentException 不正な引数、または不適切な引数がメソッドに渡されたことを示すために投げられる RuntimeException
IllegalStateException 不正または不適切な時点でメソッドが呼び出されたことを示す。つまり、要求された操作に対して環境やアプリケーションが適切な状態にない RuntimeException

どちらも非検査例外(RuntimeException の子)なので、throws を書かなくても投げられ、呼び出し側に try-catch を強制しません。「呼び出し方を直してもらうための例外」という位置づけです。

比べる点 IllegalArgumentException IllegalStateException
原因のある場所 呼び出し側が渡した値 オブジェクトのその時点の状態
直し方 渡す値を変える 呼ぶ順番や回数を変える、状態を先に整える
チェックを書く場所 メソッドの先頭(引数を使う前) メソッドの先頭(状態を変える前)
メッセージに入れるもの 引数の名前、受け付ける条件、実際の値 今の状態、必要な状態
よくある場面 コンストラクタ、setter、変換メソッド start/stop、open/close、初期化前の利用

例で見る:在庫クラスでの使い分け

import java.util.Objects;

public class Stock {
    private final String itemName;
    private int quantity;
    private boolean closed;

    public Stock(String itemName, int quantity) {
        // null は NullPointerException(Objects.requireNonNull が投げる)
        this.itemName = Objects.requireNonNull(itemName, "itemName");
        // 値の範囲は IllegalArgumentException
        if (itemName.isBlank()) {
            throw new IllegalArgumentException("itemName が空です");
        }
        if (quantity < 0) {
            throw new IllegalArgumentException("quantity は0以上にしてください: " + quantity);
        }
        this.quantity = quantity;
    }

    public void ship(int amount) {
        // 呼ぶタイミング(オブジェクトの状態)は IllegalStateException
        if (closed) {
            throw new IllegalStateException("締め処理の後は出庫できません: " + itemName);
        }
        if (amount <= 0) {
            throw new IllegalArgumentException("amount は1以上にしてください: " + amount);
        }
        // 引数を変えれば通る → 引数側の問題として扱う
        if (amount > quantity) {
            throw new IllegalArgumentException("amount が在庫数を超えています: amount=" + amount + ", 在庫=" + quantity);
        }
        quantity -= amount;
    }

    public void close() {
        if (closed) {
            throw new IllegalStateException("すでに締め処理が済んでいます: " + itemName);
        }
        closed = true;
    }

    static void show(Runnable r) {
        try {
            r.run();
            System.out.println("OK");
        } catch (RuntimeException e) {
            System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        show(() -> new Stock(null, 10));
        show(() -> new Stock(" ", 10));
        show(() -> new Stock("ノート", -1));

        Stock stock = new Stock("ノート", 10);
        show(() -> stock.ship(0));
        show(() -> stock.ship(11));
        show(() -> stock.ship(3));
        show(stock::close);
        show(() -> stock.ship(1));
        show(stock::close);
    }
}
NullPointerException: itemName
IllegalArgumentException: itemName が空です
IllegalArgumentException: quantity は0以上にしてください: -1
IllegalArgumentException: amount は1以上にしてください: 0
IllegalArgumentException: amount が在庫数を超えています: amount=11, 在庫=10
OK
OK
IllegalStateException: 締め処理の後は出庫できません: ノート
IllegalStateException: すでに締め処理が済んでいます: ノート

同じ ship メソッドの中でも、原因によって例外を分けています。 締め処理の後はどんな数量を渡しても出庫できないので IllegalStateException、数量が0以下や在庫を超える場合は「数量を変えれば通る」ので IllegalArgumentException です。

迷いやすい境目

場面 選ぶ例外 考え方
引数が null NullPointerException 標準APIの Objects.requireNonNull や List.of がこの例外を投げる。そろえると読む人が迷わない
引数の値は正しい形だが、今の状態と合わない(在庫10に対して11) IllegalArgumentException 別の値なら通るので引数側の問題
状態のせいで、どの値でも通らない(締め処理の後) IllegalStateException 引数を変えても解決しない
設定やフィールドが初期化されていない IllegalStateException 引数のないメソッドでも起こる
位置を表す引数が範囲外 IndexOutOfBoundsException 「Index 5 out of bounds for length 3」と位置と長さが伝わる
読み取り専用の実装で変更メソッドを呼ばれた UnsupportedOperationException 状態が変わっても使えるようにならない操作

標準APIが実際に投げる例(実行結果つき)

「標準APIではどちらを使っているか」を知っておくと、自分のコードでも選びやすくなります。次のプログラムは、わざと例外になる呼び出しを並べ、例外のクラス名とメッセージを表示します。

import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StdThrows {
    interface Action { void run() throws Exception; }
    enum Color { RED, BLUE }

    static void show(String label, Action action) {
        try {
            action.run();
            System.out.println(label + " => 例外なし");
        } catch (Exception e) {
            System.out.println(label + " => " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        System.out.println("--- 引数が合わない(IllegalArgumentException とその子クラス)");
        show("new ArrayList<>(-1)", () -> new ArrayList<String>(-1));
        show("Thread.sleep(-1)", () -> Thread.sleep(-1));
        show("new Random().nextInt(0)", () -> new Random().nextInt(0));
        show("\"ab\".repeat(-1)", () -> "ab".repeat(-1));
        show("Color.valueOf(\"GREEN\")", () -> Color.valueOf("GREEN"));
        show("UUID.fromString(\"abc\")", () -> UUID.fromString("abc"));
        show("Integer.parseInt(\"12a\")", () -> Integer.parseInt("12a"));
        show("String.format(\"%q\", 1)", () -> String.format("%q", 1));
        show("list.subList(2, 1)", () -> new ArrayList<>(List.of(1, 2, 3)).subList(2, 1));

        System.out.println("--- 呼ぶタイミングが合わない(IllegalStateException)");
        show("Iterator.remove() を2回", () -> {
            Iterator<String> it = new ArrayList<>(List.of("a", "b")).iterator();
            it.next();
            it.remove();
            it.remove();
        });
        show("Stream を2回使う", () -> {
            Stream<String> s = Stream.of("a", "b");
            s.count();
            s.count();
        });
        show("閉じた Scanner で nextLine()", () -> {
            Scanner sc = new Scanner("x");
            sc.close();
            sc.nextLine();
        });
        show("find() の前に Matcher.group()", () -> {
            Matcher m = Pattern.compile("a").matcher("abc");
            m.group();
        });
        show("Collectors.toMap でキー重複", () ->
            Stream.of("a1", "a2").collect(Collectors.toMap(s -> s.charAt(0), s -> s)));
        show("満杯のキューに add()", () -> {
            Queue<String> q = new ArrayBlockingQueue<>(1);
            q.add("a");
            q.add("b");
        });
        show("同じ Thread を2回 start()", () -> {
            Thread t = new Thread(() -> {});
            t.start();
            t.start();
        });

        System.out.println("--- null(NullPointerException)");
        show("Objects.requireNonNull(null, \"name\")", () -> Objects.requireNonNull(null, "name"));
        show("List.of(\"a\", null)", () -> List.of("a", null));
        show("Map.of(\"k\", null)", () -> Map.of("k", null));

        System.out.println("--- 位置が範囲外(IndexOutOfBoundsException とその子クラス)");
        show("Objects.checkIndex(5, 3)", () -> Objects.checkIndex(5, 3));
        show("List.of(1, 2, 3).get(5)", () -> List.of(1, 2, 3).get(5));
        show("長さ3の配列で a[5]", () -> { int[] a = new int[3]; a[5] = 1; });
        show("\"abc\".substring(5)", () -> "abc".substring(5));

        System.out.println("--- その操作を用意していない(UnsupportedOperationException)");
        show("List.of(1).add(2)", () -> List.of(1).add(2));
        show("Arrays.asList(1, 2).add(3)", () -> Arrays.asList(1, 2).add(3));
        show("Stream.toList() の結果に add()", () -> Stream.of(1).toList().add(2));

        System.out.println("--- 要素がない(NoSuchElementException)");
        show("Optional.empty().orElseThrow()", () -> Optional.empty().orElseThrow());
    }
}
--- 引数が合わない(IllegalArgumentException とその子クラス)
new ArrayList<>(-1) => java.lang.IllegalArgumentException: Illegal Capacity: -1
Thread.sleep(-1) => java.lang.IllegalArgumentException: timeout value is negative
new Random().nextInt(0) => java.lang.IllegalArgumentException: bound must be positive
"ab".repeat(-1) => java.lang.IllegalArgumentException: count is negative: -1
Color.valueOf("GREEN") => java.lang.IllegalArgumentException: No enum constant StdThrows.Color.GREEN
UUID.fromString("abc") => java.lang.IllegalArgumentException: Invalid UUID string: abc
Integer.parseInt("12a") => java.lang.NumberFormatException: For input string: "12a"
String.format("%q", 1) => java.util.UnknownFormatConversionException: Conversion = 'q'
list.subList(2, 1) => java.lang.IllegalArgumentException: fromIndex(2) > toIndex(1)
--- 呼ぶタイミングが合わない(IllegalStateException)
Iterator.remove() を2回 => java.lang.IllegalStateException: null
Stream を2回使う => java.lang.IllegalStateException: stream has already been operated upon or closed
閉じた Scanner で nextLine() => java.lang.IllegalStateException: Scanner closed
find() の前に Matcher.group() => java.lang.IllegalStateException: No match found
Collectors.toMap でキー重複 => java.lang.IllegalStateException: Duplicate key a (attempted merging values a1 and a2)
満杯のキューに add() => java.lang.IllegalStateException: Queue full
同じ Thread を2回 start() => java.lang.IllegalThreadStateException: null
--- null(NullPointerException)
Objects.requireNonNull(null, "name") => java.lang.NullPointerException: name
List.of("a", null) => java.lang.NullPointerException: null
Map.of("k", null) => java.lang.NullPointerException: null
--- 位置が範囲外(IndexOutOfBoundsException とその子クラス)
Objects.checkIndex(5, 3) => java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 3
List.of(1, 2, 3).get(5) => java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
長さ3の配列で a[5] => java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
"abc".substring(5) => java.lang.StringIndexOutOfBoundsException: Range [5, 3) out of bounds for length 3
--- その操作を用意していない(UnsupportedOperationException)
List.of(1).add(2) => java.lang.UnsupportedOperationException: null
Arrays.asList(1, 2).add(3) => java.lang.UnsupportedOperationException: null
Stream.toList() の結果に add() => java.lang.UnsupportedOperationException: null
--- 要素がない(NoSuchElementException)
Optional.empty().orElseThrow() => java.util.NoSuchElementException: No value present

結果から読み取れることをまとめます。

気づき 実例
負の数や0など、値の範囲は IllegalArgumentException Illegal Capacity: -1、bound must be positive、count is negative: -1
メッセージには実際の値が入っているものが多い Invalid UUID string: abc、fromIndex(2) > toIndex(1)
同じ操作の繰り返し・順番の違いは IllegalStateException Iterator.remove() の2回目、Stream の2回目、find() の前の group()
メッセージが null の例外もある Iterator.remove()、List.of(1).add(2) などはメッセージなし。スタックトレースの行で場所を確かめる
null は NullPointerException List.of("a", null)、Map.of("k", null)
Thread を2回 start() したときの IllegalThreadStateException は、IllegalArgumentException の子クラス 名前に State が付くが、親は IllegalArgumentException(次の節で確認)
list.subList(2, 1) は位置の引数だが IllegalArgumentException 「開始が終了より後」という引数どうしの組み合わせが原因のため

Stream の再利用と Iterator.remove の直し方

特によく出会う2つの IllegalStateException は、次のように直します。

エラー文 原因 直し方
stream has already been operated upon or closed 同じ Stream に終端操作(count、forEach、collect など)を2回呼んだ 必要なたびに list.stream() で作り直す。結果を使い回すなら toList() でリストにしておく
IllegalStateException(メッセージなし、Iterator.remove) next() を呼ばずに remove()、または1回の next() に対して remove() を2回 next() 1回につき remove() は1回。条件で消すなら list.removeIf(...)
Duplicate key a (attempted merging values a1 and a2) Collectors.toMap でキーが重なった 第3引数に (a, b) -> a などの重なったときの扱いを渡す

継承関係:catch するときに知っておくこと

import java.util.List;

public class Hierarchy {
    static String chain(Class<?> c) {
        StringBuilder sb = new StringBuilder(c.getSimpleName());
        for (Class<?> p = c.getSuperclass(); p != Exception.class && p != null; p = p.getSuperclass()) {
            sb.append(" → ").append(p.getSimpleName());
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        List<Class<?>> list = List.of(
            IllegalArgumentException.class,
            NumberFormatException.class,
            IllegalThreadStateException.class,
            java.util.regex.PatternSyntaxException.class,
            java.nio.file.InvalidPathException.class,
            IllegalStateException.class,
            java.util.concurrent.CancellationException.class,
            NullPointerException.class,
            IndexOutOfBoundsException.class,
            ArrayIndexOutOfBoundsException.class,
            UnsupportedOperationException.class,
            java.util.NoSuchElementException.class);
        for (Class<?> c : list) {
            System.out.println(chain(c));
        }

        // 親で catch すると子クラスも受け取る
        try {
            Integer.parseInt("12a");
        } catch (IllegalArgumentException e) {
            System.out.println("IllegalArgumentException で受け取った: " + e.getClass().getSimpleName());
        }
    }
}
IllegalArgumentException → RuntimeException
NumberFormatException → IllegalArgumentException → RuntimeException
IllegalThreadStateException → IllegalArgumentException → RuntimeException
PatternSyntaxException → IllegalArgumentException → RuntimeException
InvalidPathException → IllegalArgumentException → RuntimeException
IllegalStateException → RuntimeException
CancellationException → IllegalStateException → RuntimeException
NullPointerException → RuntimeException
IndexOutOfBoundsException → RuntimeException
ArrayIndexOutOfBoundsException → IndexOutOfBoundsException → RuntimeException
UnsupportedOperationException → RuntimeException
NoSuchElementException → RuntimeException
IllegalArgumentException で受け取った: NumberFormatException
  • NumberFormatException は IllegalArgumentException の子クラスです。catch (IllegalArgumentException e) と書くと、Integer.parseInt の例外も一緒に受け取ります
  • IllegalThreadStateException、PatternSyntaxException、InvalidPathException も IllegalArgumentException の子です。API ドキュメントには直接の子クラスが17個載っています
  • IllegalStateException の子には CancellationException やなどがあります
  • NullPointerException と IndexOutOfBoundsException は、IllegalArgumentException の子ではありません。catch (IllegalArgumentException e) では受け取れないので、まとめて受けたいときは catch (IllegalArgumentException | NullPointerException e) のように並べます

Objects.requireNonNull と Objects.checkIndex の使い方

java.util.Objects には、引数チェック用のメソッドがそろっています。API ドキュメントにも、requireNonNull は「主にメソッドやコンストラクタでの引数の検証のために設計されている」と書かれています。

import java.util.List;
import java.util.Objects;

public class ObjectsDemo {
    static void show(String label, Runnable r) {
        try {
            r.run();
        } catch (RuntimeException e) {
            System.out.println(label + " => " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        String name = null;
        List<String> list = List.of("a", "b", "c");

        // null チェック
        show("requireNonNull(x)", () -> Objects.requireNonNull(name));
        show("requireNonNull(x, \"name\")", () -> Objects.requireNonNull(name, "name"));
        show("requireNonNull(x, supplier)", () -> Objects.requireNonNull(name, () -> "name は必須です(会員ID=" + 42 + ")"));
        System.out.println(Objects.requireNonNullElse(name, "名前なし"));
        System.out.println(Objects.requireNonNull("sato", "name"));   // null でなければその値を返す

        // 位置のチェック(Java 9以降)
        System.out.println(Objects.checkIndex(2, list.size()));        // 通ればその位置を返す
        show("checkIndex(3, 3)", () -> Objects.checkIndex(3, list.size()));
        show("checkIndex(-1, 3)", () -> Objects.checkIndex(-1, list.size()));
        show("checkFromToIndex(1, 5, 3)", () -> Objects.checkFromToIndex(1, 5, list.size()));
        show("checkFromToIndex(2, 1, 3)", () -> Objects.checkFromToIndex(2, 1, list.size()));
        show("checkFromIndexSize(1, 3, 3)", () -> Objects.checkFromIndexSize(1, 3, list.size()));
    }
}
requireNonNull(x) => NullPointerException: null
requireNonNull(x, "name") => NullPointerException: name
requireNonNull(x, supplier) => NullPointerException: name は必須です(会員ID=42)
名前なし
sato
2
checkIndex(3, 3) => IndexOutOfBoundsException: Index 3 out of bounds for length 3
checkIndex(-1, 3) => IndexOutOfBoundsException: Index -1 out of bounds for length 3
checkFromToIndex(1, 5, 3) => IndexOutOfBoundsException: Range [1, 5) out of bounds for length 3
checkFromToIndex(2, 1, 3) => IndexOutOfBoundsException: Range [2, 1) out of bounds for length 3
checkFromIndexSize(1, 3, 3) => IndexOutOfBoundsException: Range [1, 1 + 3) out of bounds for length 3
メソッド 使える版 合わないときの例外 使いどころ
Objects.requireNonNull(obj) Java 7 NullPointerException(メッセージなし) 引数が1つだけのとき
Objects.requireNonNull(obj, "name") Java 7 NullPointerException: name 引数が複数あるとき。どの引数かをメッセージに入れる
Objects.requireNonNull(obj, () -> "…") Java 8 同上 メッセージの組み立てを、null だったときだけ行いたいとき
Objects.requireNonNullElse(obj, 既定値) Java 9 (両方 null のときだけ NullPointerException) null なら既定値を使いたいとき
Objects.checkIndex(index, length) Java 9 IndexOutOfBoundsException 0 以上 length 未満かを確かめる
Objects.checkFromToIndex(from, to, length) Java 9 IndexOutOfBoundsException 開始〜終了(終了は含まない)の範囲
Objects.checkFromIndexSize(from, size, length) Java 9 IndexOutOfBoundsException 開始位置と件数の範囲

requireNonNull はnull でなければ受け取った値をそのまま返すので、this.name = Objects.requireNonNull(name, "name"); と代入と同時に書けます。checkIndex も通ればその位置を返します。

コピペ用:自作の引数チェック用メソッド

if (...) throw new IllegalArgumentException(...) が何度も出てくるようになったら、小さなユーティリティにまとめると、例外の種類とメッセージの形がそろいます。

import java.util.Collection;

/** 引数と状態のチェックをまとめた小さなユーティリティ。 */
public final class Checks {
    private Checks() {}

    /** 引数の条件。満たさなければ IllegalArgumentException。 */
    public static void argument(boolean condition, String format, Object... args) {
        if (!condition) {
            throw new IllegalArgumentException(String.format(format, args));
        }
    }

    /** 呼ぶタイミング(状態)の条件。満たさなければ IllegalStateException。 */
    public static void state(boolean condition, String format, Object... args) {
        if (!condition) {
            throw new IllegalStateException(String.format(format, args));
        }
    }

    /** null ならば NullPointerException。null でなければそのまま返す。 */
    public static <T> T notNull(T value, String name) {
        if (value == null) {
            throw new NullPointerException(name + " は null にできません");
        }
        return value;
    }

    /** null は NullPointerException、空や空白だけは IllegalArgumentException。 */
    public static String notBlank(String value, String name) {
        notNull(value, name);
        argument(!value.isBlank(), "%s は空にできません", name);
        return value;
    }

    /** 1以上。 */
    public static int positive(int value, String name) {
        argument(value > 0, "%s は1以上にしてください: %d", name, value);
        return value;
    }

    /** min 以上 max 以下。 */
    public static int inRange(int value, int min, int max, String name) {
        argument(value >= min && value <= max, "%s は%d〜%dの範囲にしてください: %d", name, min, max, value);
        return value;
    }

    /** null でも空でもないコレクション。 */
    public static <C extends Collection<?>> C notEmpty(C value, String name) {
        notNull(value, name);
        argument(!value.isEmpty(), "%s は1件以上必要です", name);
        return value;
    }
}

使う側のコードです。Checks.java と同じフォルダに置き、javac Checks.java ChecksDemo.java のあと java ChecksDemo で実行します。

import java.util.List;

public class ChecksDemo {
    static class Task {
        private final String title;
        private final int priority;
        private boolean started;
        private boolean done;

        Task(String title, int priority) {
            this.title = Checks.notBlank(title, "title");
            this.priority = Checks.inRange(priority, 1, 5, "priority");
        }

        void start() {
            Checks.state(!started, "タスク「%s」は開始済みです", title);
            started = true;
        }

        void finish() {
            Checks.state(started, "タスク「%s」はまだ開始していません", title);
            Checks.state(!done, "タスク「%s」は完了済みです", title);
            done = true;
        }
    }

    static void show(Runnable r) {
        try {
            r.run();
            System.out.println("OK");
        } catch (RuntimeException e) {
            System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        show(() -> new Task(null, 3));
        show(() -> new Task("", 3));
        show(() -> new Task("資料作成", 9));
        show(() -> Checks.positive(0, "count"));
        show(() -> Checks.notEmpty(List.of(), "members"));

        Task task = new Task("資料作成", 3);
        show(task::finish);
        show(task::start);
        show(task::start);
        show(task::finish);
        show(task::finish);
    }
}
NullPointerException: title は null にできません
IllegalArgumentException: title は空にできません
IllegalArgumentException: priority は1〜5の範囲にしてください: 9
IllegalArgumentException: count は1以上にしてください: 0
IllegalArgumentException: members は1件以上必要です
IllegalStateException: タスク「資料作成」はまだ開始していません
OK
IllegalStateException: タスク「資料作成」は開始済みです
OK
IllegalStateException: タスク「資料作成」は完了済みです

メッセージを String.format で組み立てるのは条件に合わなかったときだけなので、通常の呼び出しでは文字列の連結が走りません。

メッセージの書き方と、元の例外のつなぎ方

例外のメッセージは、ログを見た人がコードを開かなくても直せることを目標にします。

入れるもの 例
引数の名前 quantity
受け付ける条件 0以上にしてください
実際に渡された値 : -1

throw new IllegalArgumentException("不正な引数です") よりも、"quantity は0以上にしてください: -1" のほうが、原因と直し方が一目で伝わります。ただし、パスワードなど表示したくない値はメッセージに含めません。

別の例外を受けて IllegalArgumentException に言い換えるときは、元の例外を2番目の引数(cause)に渡します。スタックトレースに Caused by: として残り、最初の原因までたどれます。

public class WrapCause {
    static int parseQuantity(String text) {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            // 何の値だったのかを足し、元の例外を cause として渡す
            throw new IllegalArgumentException("数量は整数で指定してください: \"" + text + "\"", e);
        }
    }

    public static void main(String[] args) {
        System.out.println(parseQuantity("12"));
        System.out.println(parseQuantity("12個"));
    }
}
12
Exception in thread "main" java.lang.IllegalArgumentException: 数量は整数で指定してください: "12個"
    at WrapCause.parseQuantity(WrapCause.java:7)
    at WrapCause.main(WrapCause.java:13)
Caused by: java.lang.NumberFormatException: For input string: "12個"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at WrapCause.parseQuantity(WrapCause.java:4)
    ... 1 more

投げる側・受ける側のチェックリスト

立場 確かめること
投げる側 チェックはメソッドの先頭で行う(フィールドを書き換える前)
投げる側 public なメソッドとコンストラクタでは、受け付ける条件を Javadoc の @throws に書く
投げる側 null は NullPointerException、値の範囲は IllegalArgumentException、状態は IllegalStateException にそろえる
投げる側 メッセージに名前・条件・実際の値を入れる
受ける側 これらの例外は基本的に catch せず、呼び出し方を直す(プログラムの誤りを知らせる例外のため)
受ける側 利用者の入力など、合わない値が来ることが普通にある箇所は、呼ぶ前に検証するか、入口で catch して入力し直しを案内する
受ける側 Iterator なら hasNext()、Optional なら isPresent() のように、状態を先に確かめるメソッドを使う

よくある質問

Q. 引数が null のとき、IllegalArgumentException と NullPointerException のどちらを投げますか? A. 標準APIに合わせるなら NullPointerException です。Objects.requireNonNull、List.of、Map.of はいずれも NullPointerException を投げます(上の実行結果のとおり)。チームの規約で IllegalArgumentException にそろえている場合は、その規約に合わせて統一されていることが大切です。

Q. IllegalArgumentException は throws に書く必要がありますか? A. 非検査例外なので、書かなくてもコンパイルできます。メソッド宣言の throws には書かず、Javadoc の @throws IllegalArgumentException 数量が0以下の場合 のように条件を文章で残すのが一般的です。

Q. 自分で例外クラスを作るのと、IllegalArgumentException を使うのはどう分けますか? A. 呼び出し側がその例外だけを区別して受け取り、別の処理をしたい場合は自作の例外が向いています。呼び出し方の誤りを知らせるだけなら、標準の IllegalArgumentException や IllegalStateException で十分に伝わります。自作する場合も、これらを親クラスにすると意図が読み取りやすくなります。

まとめ

  • 引数を変えれば通るなら IllegalArgumentException、どの引数でも今は通らないなら IllegalStateException
  • null は NullPointerException(Objects.requireNonNull)、位置の範囲外は IndexOutOfBoundsException(Objects.checkIndex)
  • 用意していない操作は UnsupportedOperationException、取り出す要素がないときは NoSuchElementException
  • 標準APIも同じ分け方をしている。負の容量は IllegalArgumentException、Iterator.remove の2回目や Stream の2回目は IllegalStateException
  • NumberFormatException は IllegalArgumentException の子。親で catch すると一緒に受け取る
  • メッセージには引数の名前・条件・実際の値を入れ、言い換えるときは元の例外を cause に渡す
  • チェックが増えてきたら、Checks.argument / Checks.state のような小さなメソッドにまとめて形をそろえる

参考・出典

  • Java SE 21 API ドキュメント「IllegalArgumentException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/IllegalArgumentException.html
  • Java SE 21 API ドキュメント「IllegalStateException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/IllegalStateException.html
  • Java SE 21 API ドキュメント「Objects」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Objects.html
  • Java SE 21 API ドキュメント「UnsupportedOperationException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/UnsupportedOperationException.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java IllegalArgumentExceptionの使い分け早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?