Java throwとthrowsの違いとは?例外送出と宣言の役割を完全解説
生徒
「Javaのコードでthrowとthrowsという似た単語を見かけるんですが、これって何が違うんですか?」
先生
「確かに似ていて混乱しやすいですね。throwは実際に例外を投げる命令で、throwsはメソッドが例外を投げる可能性があることを宣言するものです。」
生徒
「う~ん、まだよくわかりません。具体的にどう使い分けるんですか?」
先生
「それでは、例外処理の基本から順番に見ていきましょう。実際のコード例を交えながら説明しますね!」
1. Javaの例外処理とは?基本を理解しよう
Javaプログラムを実行している最中に、予期しないエラーや問題が発生することがあります。例えば、ファイルが見つからない、数値を0で割ろうとする、配列の範囲外にアクセスするなどです。このような異常事態のことを例外(Exception)と呼びます。
Javaでは例外が発生すると、プログラムが突然終了してしまうことがあります。これを防ぐために、例外処理という仕組みが用意されています。例外処理を適切に行うことで、エラーが発生してもプログラムを安全に継続したり、適切なエラーメッセージを表示したりできます。
例外処理には主に3つの要素があります。
- try-catch文:例外が発生する可能性のあるコードを囲み、例外をキャッチする
- throw文:意図的に例外を投げる(発生させる)
- throws宣言:メソッドが例外を投げる可能性があることを宣言する
今回は、この中でも混同しやすいthrowとthrowsの違いについて詳しく解説していきます。
2. throwとは?例外を実際に投げる命令
throwは、プログラムの中で実際に例外を投げる(送出する)ためのキーワードです。例外を投げるというのは、「ここで問題が発生しました」とJavaに知らせる行為です。throwの後ろには、投げたい例外オブジェクトを指定します。
throwは主に以下のような場面で使用します。
- 引数のバリデーション(妥当性チェック)で不正な値を検出した時
- ビジネスロジック上で処理を続けられない状況になった時
- 独自の例外を発生させたい時
それでは、実際のコード例を見てみましょう。
public class ThrowExample {
public static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上である必要があります");
}
System.out.println("年齢: " + age + "歳");
}
public static void main(String[] args) {
checkAge(25); // 正常に動作
checkAge(-5); // 例外が投げられる
}
}
このコードでは、checkAgeメソッドで年齢が負の数の場合、throwを使ってIllegalArgumentExceptionという例外を投げています。throwの後ろにはnewキーワードで例外オブジェクトを生成して指定します。
実行結果は以下のようになります。
年齢: 25歳
Exception in thread "main" java.lang.IllegalArgumentException: 年齢は0以上である必要があります
throwが実行されると、その時点でメソッドの処理は中断され、例外が呼び出し元に伝播していきます。これが例外の送出という仕組みです。
3. throwsとは?例外を投げる可能性を宣言する
一方、throwsはメソッドのシグネチャ(メソッドの定義部分)に記述するキーワードで、「このメソッドは例外を投げる可能性があります」ということを宣言するものです。throwsは実際に例外を投げるわけではなく、あくまで宣言するだけです。
throwsを使う主な理由は以下の通りです。
- チェック例外(検査例外)を投げる場合、必ず
throwsで宣言する必要がある - メソッドを使う側に、例外処理が必要であることを知らせる
- 例外処理を呼び出し元に委譲する
Javaの例外には、チェック例外と非チェック例外の2種類があります。チェック例外(IOException、SQLExceptionなど)は、必ずtry-catchで処理するか、throwsで宣言する必要があります。非チェック例外(RuntimeExceptionのサブクラス)は、宣言が必須ではありません。
具体的なコード例を見てみましょう。
import java.io.*;
public class ThrowsExample {
public static void readFile(String filename) throws IOException {
FileReader reader = new FileReader(filename);
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
System.out.println(line);
br.close();
}
public static void main(String[] args) {
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("ファイルの読み込みに失敗しました: " + e.getMessage());
}
}
}
この例では、readFileメソッドにthrows IOExceptionと宣言しています。ファイル操作はIOExceptionというチェック例外を投げる可能性があるため、throwsでの宣言が必要です。そして、mainメソッドでこのメソッドを呼び出す際には、try-catchで例外処理を行っています。
4. throwとthrowsの具体的な違いを整理
ここまでの説明を踏まえて、throwとthrowsの違いを表で整理してみましょう。
| 項目 | throw | throws |
|---|---|---|
| 役割 | 例外を実際に投げる(送出する) | 例外を投げる可能性があることを宣言する |
| 記述場所 | メソッドの中(処理の途中) | メソッドのシグネチャ(定義部分) |
| 後ろに続くもの | 例外オブジェクトのインスタンス | 例外クラスの名前(複数可) |
| 使用回数 | メソッド内で複数回使用可能 | メソッドごとに1回のみ |
| 必須かどうか | 例外を投げたいときに使う | チェック例外を投げる場合は必須 |
簡単に言えば、throwは「例外を投げる実行文」であり、throwsは「例外を投げるかもしれないという宣言」です。この違いを理解することが、Javaの例外処理を正しく使いこなす第一歩となります。
5. throwとthrowsを組み合わせた実践例
実際のプログラムでは、throwとthrowsを組み合わせて使うことがよくあります。メソッドの中でthrowを使って例外を投げ、そのメソッドのシグネチャにthrowsで宣言するというパターンです。
以下は、銀行口座の残高チェックを行うプログラム例です。
public class BankAccount {
private int balance;
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
public void withdraw(int amount) throws IllegalArgumentException {
if (amount < 0) {
throw new IllegalArgumentException("引き出し額は正の数である必要があります");
}
if (amount > balance) {
throw new IllegalArgumentException("残高が不足しています");
}
balance -= amount;
System.out.println(amount + "円引き出しました。残高: " + balance + "円");
}
public static void main(String[] args) {
BankAccount account = new BankAccount(10000);
try {
account.withdraw(3000);
account.withdraw(-500);
} catch (IllegalArgumentException e) {
System.out.println("エラー: " + e.getMessage());
}
}
}
このコードでは、withdrawメソッドの中で引き出し額が負の数の場合や残高不足の場合にthrowを使って例外を投げています。そして、メソッドのシグネチャにはthrows IllegalArgumentExceptionと宣言しています。
実行結果は以下のようになります。
3000円引き出しました。残高: 7000円
エラー: 引き出し額は正の数である必要があります
このように、throwで実際に例外を投げ、throwsで例外を投げる可能性を宣言することで、安全で保守性の高いプログラムを作ることができます。
6. 複数の例外をthrowsで宣言する方法
メソッドが複数の種類の例外を投げる可能性がある場合、throwsの後ろにカンマ区切りで複数の例外クラスを指定できます。これは、異なる種類のエラーが発生する可能性があるメソッドで使用します。
例えば、ファイルを読み込んで数値に変換する処理では、ファイルが見つからない例外と数値変換の例外の両方が発生する可能性があります。
import java.io.*;
public class MultipleExceptions {
public static int readNumberFromFile(String filename)
throws IOException, NumberFormatException {
FileReader reader = new FileReader(filename);
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
br.close();
return Integer.parseInt(line);
}
public static void main(String[] args) {
try {
int number = readNumberFromFile("number.txt");
System.out.println("読み込んだ数値: " + number);
} catch (IOException e) {
System.out.println("ファイル読み込みエラー: " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("数値変換エラー: " + e.getMessage());
}
}
}
このコードでは、readNumberFromFileメソッドがIOExceptionとNumberFormatExceptionの2つの例外を投げる可能性があることを宣言しています。呼び出し側では、それぞれの例外に対して個別の処理を行うことができます。
複数の例外を宣言することで、メソッドを使う側は、どのような問題が発生する可能性があるのかを事前に把握でき、適切な例外処理を実装できます。
7. 独自の例外クラスを作成してthrowで投げる
Javaには標準で多くの例外クラスが用意されていますが、アプリケーション固有のエラー状況を表現するために、独自の例外クラスを作成することもできます。独自例外を作成することで、より具体的で分かりやすいエラー処理が可能になります。
独自の例外クラスは、ExceptionクラスまたはRuntimeExceptionクラスを継承して作成します。Exceptionを継承するとチェック例外となり、RuntimeExceptionを継承すると非チェック例外となります。
以下は、年齢制限をチェックする独自例外の例です。
class AgeRestrictionException extends Exception {
public AgeRestrictionException(String message) {
super(message);
}
}
public class CustomExceptionExample {
public static void checkAdult(int age) throws AgeRestrictionException {
if (age < 20) {
throw new AgeRestrictionException("20歳未満は利用できません。現在の年齢: " + age + "歳");
}
System.out.println("年齢確認OK: " + age + "歳");
}
public static void main(String[] args) {
try {
checkAdult(25);
checkAdult(18);
} catch (AgeRestrictionException e) {
System.out.println("年齢制限エラー: " + e.getMessage());
}
}
}
実行結果は以下のようになります。
年齢確認OK: 25歳
年齢制限エラー: 20歳未満は利用できません。現在の年齢: 18歳
独自の例外クラスAgeRestrictionExceptionを作成し、throwで投げています。また、メソッドのシグネチャにはthrows AgeRestrictionExceptionと宣言しています。このように独自例外を使うことで、アプリケーションのドメイン固有のエラーを明確に表現できます。
8. 例外の再スローとthrowsの関係
例外をキャッチした後、再度その例外を投げることを「再スロー」といいます。これは、例外をキャッチして何か処理を行った後、さらに上位の呼び出し元にも例外を伝えたい場合に使用します。再スローを行う場合も、throwとthrowsを適切に使い分ける必要があります。
再スローは主に以下のような場面で使われます。
- 例外をログに記録した後、上位に伝播させたい場合
- 例外を別の例外にラップして投げ直したい場合
- 一部の例外処理を行ってから、最終的な処理を呼び出し元に委ねたい場合
以下は、例外を再スローする例です。
public class RethrowExample {
public static void processData(String data) throws Exception {
try {
if (data == null) {
throw new NullPointerException("データがnullです");
}
System.out.println("データ処理中: " + data);
} catch (NullPointerException e) {
System.out.println("エラーをログに記録: " + e.getMessage());
throw e; // 例外を再スロー
}
}
public static void main(String[] args) {
try {
processData(null);
} catch (Exception e) {
System.out.println("最終的な例外処理: " + e.getMessage());
}
}
}
このコードでは、processDataメソッド内で例外をキャッチしてログに記録した後、throw eで再スローしています。メソッドにはthrows Exceptionと宣言されており、呼び出し元のmainメソッドで最終的な例外処理が行われます。
再スローを使うことで、複数の階層で例外処理を行うことができ、より柔軟なエラーハンドリングが可能になります。
9. チェック例外と非チェック例外におけるthrowsの扱い
Javaの例外は、チェック例外(検査例外)と非チェック例外(非検査例外)に分類されます。この分類によって、throwsの扱いが異なります。
チェック例外は、Exceptionクラスを継承し、RuntimeExceptionを継承していない例外です。代表的なものにIOException、SQLException、ClassNotFoundExceptionなどがあります。チェック例外を投げるメソッドは、必ずthrowsで宣言するか、メソッド内でtry-catchで処理する必要があります。
非チェック例外は、RuntimeExceptionクラスを継承する例外です。代表的なものにNullPointerException、IllegalArgumentException、ArrayIndexOutOfBoundsExceptionなどがあります。非チェック例外は、throwsでの宣言が必須ではなく、宣言してもしなくても構いません。
以下のコードで、チェック例外と非チェック例外の扱いの違いを確認してみましょう。
import java.io.*;
public class ExceptionTypeExample {
// チェック例外 - throws宣言が必須
public static void checkedExceptionMethod() throws IOException {
throw new IOException("チェック例外が発生しました");
}
// 非チェック例外 - throws宣言は任意
public static void uncheckedExceptionMethod() {
throw new IllegalArgumentException("非チェック例外が発生しました");
}
public static void main(String[] args) {
// チェック例外は必ずtry-catchで処理するか、さらにthrowsで宣言が必要
try {
checkedExceptionMethod();
} catch (IOException e) {
System.out.println("キャッチ: " + e.getMessage());
}
// 非チェック例外はtry-catchが必須ではない(推奨はされる)
try {
uncheckedExceptionMethod();
} catch (IllegalArgumentException e) {
System.out.println("キャッチ: " + e.getMessage());
}
}
}
チェック例外は、コンパイル時に例外処理が強制されるため、プログラマーが必ず対処しなければなりません。一方、非チェック例外は、実行時に発生する可能性があるものの、コンパイル時にはチェックされません。これは、プログラムのバグに起因することが多く、実行前に予防すべきものだからです。
適切に例外の種類を理解し、throwとthrowsを使い分けることで、堅牢なプログラムを作成できます。
10. throwとthrowsを使う際のベストプラクティス
最後に、throwとthrowsを使う際のベストプラクティスをいくつか紹介します。これらを守ることで、保守性が高く、エラーに強いプログラムを書くことができます。
ベストプラクティス
- 適切な例外クラスを選択する:例外の種類に応じて、最も適切な例外クラスを選びましょう。引数のエラーなら
IllegalArgumentException、状態のエラーならIllegalStateExceptionなど、状況に合ったものを使います。 - 例外メッセージを分かりやすくする:
throwで例外を投げる際は、問題の内容が明確に分かるメッセージを含めましょう。デバッグやエラー対応が格段に楽になります。 - 過度に広範な例外を宣言しない:
throws Exceptionのように広範な例外を宣言すると、呼び出し側でどの例外が発生するか分かりにくくなります。できるだけ具体的な例外クラスを宣言しましょう。 - 例外を握りつぶさない:
catchブロックで例外をキャッチしたら、適切に処理するか、再スローしましょう。空のcatchブロックは問題を隠してしまいます。 - finally句でリソースを解放する:ファイルやデータベース接続などのリソースは、
finally句やtry-with-resources文で確実に解放しましょう。
例外処理は、プログラムの品質と信頼性を大きく左右する重要な要素です。throwとthrowsの違いを正しく理解し、適切に使い分けることで、エラーに強く、メンテナンスしやすいプログラムを作成できます。
最初は難しく感じるかもしれませんが、実際にコードを書きながら練習することで、自然と使い分けられるようになります。まずは簡単な例外処理から始めて、徐々に複雑なケースにも対応できるようにしていきましょう。