Javaを用いたアプリケーション開発において、予期せぬエラーや異常事態を制御するための「例外処理」は、システムの信頼性を担保する上で極めて重要な要素です。

Javaには例外を扱うためのキーワードがいくつか用意されていますが、中でも混同しやすいのが「throw」と「throws」の使い分けです。

どちらも例外に関連する単語ですが、その役割は「例外を発生させること」と「例外が発生する可能性を宣言すること」という明確な違いがあります。

本記事では、Java初心者から中級者の方に向けて、これら2つのキーワードの定義、具体的な実装方法、そして実務で役立つ設計のポイントを詳しく解説します。

Javaにおける例外処理の基本概念

Javaの例外処理を深く理解するためには、まずJavaがどのようにエラーを分類しているかを知る必要があります。

Javaの例外クラスは、すべてjava.lang.Throwableクラスを継承しています。

例外の階層構造と分類

Javaの例外は大きく分けて「Error(エラー)」と「Exception(例外)」の2つに分類されます。

Error(エラー)

メモリ不足(OutOfMemoryError)など、プログラム側では回復が不可能な致命的な問題。

Exception(例外)

プログラム内で対処可能な問題。

さらに、Exceptionはさらに以下の2種類に分かれます。

検査例外(Checked Exception)

コンパイル時にチェックされる例外です。

ファイルの読み書き(IOException)やデータベース接続(SQLException)などが該当します。

開発者は、これらの例外が発生する可能性があるコードに対して、必ずtry-catch文で処理するか、throwsキーワードで呼び出し元に伝播させる必要があります。

非検査例外(Unchecked Exception)

実行時例外(RuntimeException)とも呼ばれます。

NullPointerExceptionやArrayIndexOutOfBoundsExceptionなどが代表例です。

これらは主にプログラミングミスに起因するため、コンパイラによる強制的なチェックは行われません。

この例外の分類を理解しておくことが、throwthrowsを正しく使い分けるための第一歩となります。

throwキーワード:例外を明示的に発生させる

throwは、メソッド内で意図的に例外を「投げる(発生させる)」ために使用するキーワードです。

通常、if文などの条件分岐と組み合わせて、特定の業務ルールに反した場合や、不正な値が渡された場合に例外を生成します。

throwの基本的な構文

throwの後には、例外クラスのインスタンス(オブジェクト)を指定します。

Java
// 基本的な書き方
throw new Exceptionクラス名("エラーメッセージ");

throwの使用例

例えば、年齢を受け取るメソッドで、マイナスの値が入力された場合にIllegalArgumentExceptionを発生させるコードを見てみましょう。

Java
public class ThrowExample {
    public static void main(String[] args) {
        try {
            // 不正な値を渡してメソッドを呼び出す
            checkAge(-5);
        } catch (IllegalArgumentException e) {
            // throwされた例外をキャッチして処理する
            System.out.println("例外をキャッチしました: " + e.getMessage());
        }
    }

    /**
     年齢をチェックするメソッド
     @param age 年齢
     */
    public static void checkAge(int age) {
        if (age < 0) {
            // 条件に合致する場合、明示的に例外を発生させる
            throw new IllegalArgumentException("年齢に負の値は指定できません。");
        }
        System.out.println("年齢は " + age + " 歳です。");
    }
}
実行結果
例外をキャッチしました: 年齢に負の値は指定できません。

この例では、age < 0 という条件が満たされた瞬間に、throwによって例外オブジェクトが生成され、通常の処理フローが中断されます。

投げられた例外は、呼び出し元のcatchブロックで捕捉されます。

throwを使用する主な場面

バリデーションチェック

メソッドの引数が期待する範囲外である場合。

業務ロジックエラー

在庫不足や権限不足など、プログラムの動作としては正常だが業務的にエラーとみなすべき場合。

例外の再スロー

一度catchした例外に対してログ出力などの共通処理を行い、再度上位のメソッドへ投げ直す場合。

throwsキーワード:例外の発生可能性を宣言する

一方のthrowsは、そのメソッドが特定の例外を投げる可能性があることを「宣言する」ために使用するキーワードです。

メソッドの定義(シグネチャ)の一部として記述され、そのメソッドの利用者に「このメソッドを使うなら、これらの例外に対処してください」という注意喚起の役割を果たします。

throwsの基本的な構文

throwsはメソッド名の後ろ、波括弧の前に記述します。

複数の例外を宣言する場合はカンマで区切ります。

Java
修飾子 戻り値型 メソッド名(引数) throws 例外クラスA, 例外クラスB {
    // メソッドの本体
}

throwsの使用例

次に、ファイルを読み込む処理など、検査例外が発生する可能性のあるメソッドの例を見てみましょう。

Java
import java.io.IOException;

public class ThrowsExample {
    public static void main(String[] args) {
        try {
            // throwsが宣言されているメソッドを呼び出す
            readFile("config.txt");
        } catch (IOException e) {
            // 呼び出し元で例外処理を強制される
            System.err.println("ファイル読み込み中にエラーが発生しました: " + e.getMessage());
        }
    }

    /**
     ファイルを読み込む(ふりをする)メソッド
     検査例外であるIOExceptionを投げる可能性があることを宣言
     @param fileName ファイル名
     @throws IOException ファイルアクセスエラー時
     */
    public static void readFile(String fileName) throws IOException {
        // 本来はファイル操作などを行うが、ここではデモ用に例外を投げる
        if (fileName == null) {
            throw new IOException("ファイル名が指定されていません。");
        }
        System.out.println(fileName + " を読み込みました。");
    }
}
実行結果
config.txt を読み込みました。

この例では、readFileメソッドがIOExceptionthrowsしています。

これにより、mainメソッド内でreadFileを呼び出す際には、必ずtry-catchで囲むか、mainメソッド自身もthrowsを宣言する必要があります。

これを怠ると、コンパイルエラーが発生します。

throwsを使用する主な目的

責任の転送

メソッド内で発生した例外をその場で処理せず、より適切な判断ができる呼び出し元(上位層)に処理を任せる。

インターフェースの明示

そのメソッドがどのような異常系を持つかを外部に公開し、安全なプログラム設計を促す。

throwとthrowsの違いを比較表で整理

これら2つのキーワードは名前が似ていますが、使い所や性質は全く異なります。

以下の表で違いを整理しましょう。

比較項目throwthrows
主な役割例外を発生させる(実行)例外の発生可能性を宣言する(定義)
記述場所メソッドの中(処理ブロック内)メソッドの定義(シグネチャ)
指定対象例外のインスタンスnewしたもの)例外の型(クラス名)
指定数1回につき1つのオブジェクトカンマ区切りで複数指定可能
強制力記述した場所で即座に例外を飛ばす検査例外の場合、呼び出し元に処理を強制する

実践的な使い分け:例外の伝播設計

実際のシステム開発では、throwthrowsを組み合わせて「例外の伝播(プロパゲーション)」を設計します。

すべてをtry-catchでその場で解決するのではなく、あえて上位に投げることで、アプリケーション全体の整合性を保ちます。

階層化された例外処理の例

例えば、Webアプリケーションにおいて「コントローラー層 > サービス層 > データアクセス層(DAO)」という構成の場合を考えてみます。

DAO層

SQL実行時にエラーが発生。

DB固有のSQLExceptionthrowする、あるいはthrowsで宣言する。

サービス層

DAOが投げた例外を受け取る。

DBの細かなエラーをそのまま画面に出したくないため、独自のBusinessExceptionに包み直して(ラップして)throwする。

コントローラー層

最終的に例外をcatchし、ユーザーに分かりやすいエラー画面を表示する。

このように、throwsを使って例外を上の階層へ押し上げることで、「どこでエラーをハンドリングすべきか」という責務を明確にすることができます。

カスタム例外の作成と活用

標準のJava例外クラス(ExceptionやRuntimeExceptionなど)だけでは、何が原因でエラーが起きたのかを特定しにくい場合があります。

そのため、プロジェクト固有の例外クラスを作成することが推奨されます。

Java
// 独自の例外クラスを作成
class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

public class BankService {
    private int balance = 1000;

    // 独自の例外をthrowsに含める
    public void withdraw(int amount) throws InsufficientBalanceException {
        if (amount > balance) {
            // throwで独自の例外を発生させる
            throw new InsufficientBalanceException("残高不足です。現在の残高: " + balance);
        }
        balance -= amount;
        System.out.println("引き出し成功。残高: " + balance);
    }

    public static void main(String[] args) {
        BankService service = new BankService();
        try {
            service.withdraw(2000);
        } catch (InsufficientBalanceException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}
実行結果
エラー: 残高不足です。現在の残高: 1000

独自の例外クラスを使うことで、catchブロック側で「残高不足の場合の処理」を他の汎用的なエラーと区別して記述できるメリットがあります。

例外処理におけるアンチパターンと注意点

throwthrowsを正しく使うためには、避けるべき設計パターンも知っておく必要があります。

1. 無意味な「throws Exception」の乱用

すべてのメソッドにthrows Exception(すべての例外の親)を記述してしまうと、そのメソッドが「具体的にどのようなエラーを出す可能性があるのか」が分からなくなります。

また、呼び出し元でも具体的な対処ができず、結局すべての例外をまとめてキャッチすることになり、バグの温床となります。

可能な限り、具体的な例外クラス(例:FileNotFoundExceptionなど)を宣言するようにしましょう。

2. 例外の「握りつぶし」

catchしたものの、中身が空っぽだったり、単にe.printStackTrace()を出力するだけで終わらせたりすることを「例外の握りつぶし」と呼びます。

Java
try {
    someMethod();
} catch (Exception e) {
    // 何もしない、あるいはログを出すだけで処理を続行してしまう
}

これをやってしまうと、エラーが起きたことに気づかずに後続の処理が走り、データが不整合な状態になるリスクがあります。

適切に処理できない場合は、無理にcatchせず、throwsで上位へ投げるべきです。

3. RuntimeExceptionに対する不必要なthrows宣言

NullPointerExceptionなどの非検査例外は、throwsに記述しなくても文法上問題ありません。

これらをすべてthrowsに書くと、メソッドシグネチャが非常に長くなり、可読性が低下します。

非検査例外については、Javadocの@throwsタグを用いてドキュメント上で説明するのが一般的です。

throwとthrowsを組み合わせた高度なテクニック:例外のラップ

下位層で発生した低レベルな例外(SQLExceptionなど)を、そのまま上位層(UI層など)に伝えるのは、カプセル化の観点から好ましくありません。

そこで、下位の例外をキャッチして、より抽象度の高い独自例外に包んで再スローする手法がよく使われます。

Java
public void saveUser(User user) throws DataAccessException {
    try {
        // DB保存処理(SQLExceptionが発生する可能性があるとする)
        database.insert(user);
    } catch (SQLException e) {
        // 元の例外(cause)を保持したまま、独自の例外をthrowする
        throw new DataAccessException("ユーザー情報の保存に失敗しました", e);
    }
}

このように、new Exception(message, cause)の形式でコンストラクタに元の例外を渡すことで、「例外の連鎖(Exception Chaining)」を構築できます。

これにより、ログを確認した際に「最終的なエラーの原因」だけでなく「最初にどこで何が起きたか」というスタックトレースを追跡することが可能になります。

まとめ

Javaにおける例外処理は、単にエラーを回避するだけでなく、プログラムの意図を他の開発者やシステムに正しく伝えるためのコミュニケーション手段でもあります。

  • throwは「ここでエラーが発生した」という事実を確定させ、例外オブジェクトを具体的に生成して発射するためのものです。
  • throwsは「このメソッドは危険を伴う」という看板を掲げ、例外の処理責任を呼び出し元へ委ねるためのものです。

これら2つを適切に使い分けることで、エラーに強く、メンテナンス性の高いソースコードを書くことができます。

特に大規模なシステムになればなるほど、例外の伝播設計は重要度を増します。

まずは、自分が書いているメソッドが「その場でエラーを解決すべきか(try-catch)」、それとも「呼び出し元に任せるべきか(throws)」を意識することから始めてみてください。

Javaの例外処理をマスターすることは、プロフェッショナルなエンジニアへの大きな一歩です。

本記事で紹介した内容を参考に、日々のコーディングで正しい例外処理の実装を心がけていきましょう。