- JAVA
Java throwとthrowsの違い|例外の投げ方と独自例外の作り方
throwは「メソッドの中で例外を1つ投げる文」、throwsは「このメソッドは例外を投げることがある、とメソッドの宣言に書く目印」です。 名前は1文字違いですが、書く場所も、後ろに置くものも、役割も別のものです。throw は例外を発生させる側、throws は発生するかもしれないことを呼び出し元に知らせる側、と覚えると整理しやすくなります。
この記事では、まず2つの違いを比較表で確認し、そのあと throws を書き忘れたときのコンパイルエラー文、検査例外と非検査例外の代表例一覧、メッセージと cause を受ける独自例外クラスのひな形、例外を包んで投げ直したときの「Caused by」の読み方 を順に見ていきます。掲載しているコードはすべて OpenJDK 21.0.10(Linux)で動かし、出力をそのまま載せています。コードはコピーしてそのまま試せる完成形です。
throwとthrowsの違い早見表
| 比べる点 | throw | throws |
|---|---|---|
| 種類 | 文(処理として実行される) | メソッド宣言の一部(実行はされない) |
| 書く場所 | メソッドやコンストラクタの本体の中 | 引数リストの ) と本体の { の間 |
| 後ろに置くもの | 例外のインスタンス(new IllegalArgumentException("...") など) |
例外のクラス名(IOException など) |
| 個数 | 1つの throw 文で投げられるのは1つ | カンマ区切りで複数書ける |
| 役割 | 例外を発生させ、その場の処理を中断する | 「この例外が外へ出ることがある」と呼び出し元に知らせる |
| 実行時の動き | 以降の行は実行されず、catch を探して呼び出し元へ戻っていく | 何も起きない(コンパイラが確認に使う) |
| 書き忘れたとき | 例外は発生しないだけ | 検査例外ならコンパイルエラー |
| 英語の意味 | 投げる(命令形) | 投げる(三人称単数。「このメソッドは〜を投げる」) |
| 書き方の例 | throw new IOException("読めません"); |
void load() throws IOException { ... } |
1行でまとめると、throw の後ろは「new した例外」、throws の後ろは「例外の型名」 です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java throwとthrowsの違い|例外の投げ方と独自例外の作り方」
この記事の動作環境とコードの動かし方
| 項目 | 内容 |
|---|---|
| JDK | OpenJDK 21.0.10 |
| OS | Linux(Ubuntu 24.04 系) |
| 実行方法 | java ファイル名.java(単一ファイル実行)。スタックトレースを見せる例だけ javac でコンパイルしてから java クラス名 で実行 |
| エラー文の言語 | 英語と日本語の両方を掲載(日本語は javac -J-Duser.language=ja で出力) |
java ファイル名.java は、コンパイルと実行を1回で行う方法です。Java 11 以降で使えます。コンパイルエラーの例は javac ファイル名.java で確認しています。
throwの使い方|例外を自分で投げる
throw は、「この値では処理を続けられない」と判断した場所で、例外オブジェクトを投げる文 です。書式は throw 例外のインスタンス; で、多くの場合は throw new 例外クラス("メッセージ"); の形で書きます。
public class ThrowBasic {
static void setQuantity(int quantity) {
if (quantity < 1) {
// throw で例外オブジェクトを1つ投げる
throw new IllegalArgumentException("数量は1以上で指定してください: " + quantity);
}
System.out.println("数量を " + quantity + " に設定しました");
}
public static void main(String[] args) {
setQuantity(3);
try {
setQuantity(0);
} catch (IllegalArgumentException e) {
System.out.println("受け取った例外: " + e.getMessage());
}
setQuantity(-5); // catch しないとここでプログラムが止まる
System.out.println("この行は実行されません");
}
}
実行結果:
数量を 3 に設定しました
受け取った例外: 数量は1以上で指定してください: 0
Exception in thread "main" java.lang.IllegalArgumentException: 数量は1以上で指定してください: -5
at ThrowBasic.setQuantity(ThrowBasic.java:5)
at ThrowBasic.main(ThrowBasic.java:17)
この結果から、throw の動きが3つ読み取れます。
- throw が実行されると、その後ろの行は実行されません。
setQuantity(0)では「数量を 0 に設定しました」が表示されていません。 - 呼び出し元に catch があれば、そこで受け取れます。 2回目の呼び出しは catch に入り、メッセージを表示して処理が続いています。
- どこにも catch がなければ、プログラムはそこで止まります。 3回目の呼び出しでスタックトレースが表示され、最後の
printlnは実行されていません。
IllegalArgumentException は非検査例外(後述)なので、メソッドに throws を書かなくてもコンパイルできます。
throwのすぐ後ろに文を書くとコンパイルエラーになる
throw の直後には処理が到達しないため、同じブロックに続けて文を書くとコンパイラが教えてくれます。
public class Unreachable {
static void stop() {
throw new IllegalStateException("ここで止まる");
System.out.println("到達しない");
}
public static void main(String[] args) { stop(); }
}
Unreachable.java:4: error: unreachable statement
System.out.println("到達しない");
^
1 error
日本語表示では次のようになります。
Unreachable.java:4: エラー: この文に制御が移ることはありません
return と同じように、throw もそのブロックの最後の文にする と覚えておくと、このエラーには出会いません。
throwsの使い方|例外が出ることを宣言する
throws は、メソッドの宣言に「このメソッドからは、この型の例外が外に出ることがあります」と書く目印 です。ファイルを読む Files.readString は、宣言に throws IOException が付いています。これを呼ぶ側は、「自分で catch する」か「自分も throws を付けて、さらに呼び出し元に任せる」かのどちらかを選びます。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class WithThrows {
// throws で「このメソッドは IOException を投げることがある」と宣言する
static String readMemo(String name) throws IOException {
return Files.readString(Path.of(name));
}
public static void main(String[] args) {
try {
System.out.println(readMemo("memo.txt"));
} catch (IOException e) {
System.out.println("読み込めませんでした: " + e);
}
}
}
実行結果(memo.txt が存在しない状態で実行):
読み込めませんでした: java.nio.file.NoSuchFileException: memo.txt
readMemo は IOException を自分では処理せず、throws で呼び出し元の main に任せています。main は try-catch で受け取り、メッセージを表示しています。throws を付けたメソッドは「処理を任せる側」、catch を書いたメソッドは「処理を引き受ける側」 です。
throwsを書き忘れたときのコンパイルエラー
上のコードから throws IOException を取り除くと、コンパイルが通らなくなります。
import java.nio.file.Files;
import java.nio.file.Path;
public class NoThrows {
static String readMemo(String name) {
return Files.readString(Path.of(name));
}
public static void main(String[] args) {
System.out.println(readMemo("memo.txt"));
}
}
NoThrows.java:6: error: unreported exception IOException; must be caught or declared to be thrown
return Files.readString(Path.of(name));
^
1 error
日本語表示のとき:
NoThrows.java:6: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
return Files.readString(Path.of(name));
^
エラー1個
メッセージは 「IOException が報告されていません。catch するか、throws で宣言してください」 という意味です。エラー文がそのまま2つの直し方を示しています。
| 直し方 | 書き方 | 向いている場面 |
|---|---|---|
| catch する(must be caught) | その場を try { ... } catch (IOException e) { ... } で囲む |
そのメソッドで後始末や代わりの処理ができるとき |
| 宣言する(declared to be thrown) | メソッドに throws IOException を付ける |
対応を決めるのは呼び出し元のほうが適しているとき |
throwsを付けたメソッドを呼ぶ側にも同じエラーが出る
throws を付けると、今度は呼び出し元が「catch するか、throws を付けるか」を選ぶ番になります。どちらもしていないと、呼び出している行で同じエラーが出ます。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CallerNoCatch {
static String readMemo(String name) throws IOException {
return Files.readString(Path.of(name));
}
public static void main(String[] args) {
System.out.println(readMemo("memo.txt"));
}
}
CallerNoCatch.java:11: error: unreported exception IOException; must be caught or declared to be thrown
System.out.println(readMemo("memo.txt"));
^
1 error
エラーの行番号が、readMemo の中(6行目)から、呼び出している main の中(11行目)に移りました。throws は例外への対応を1段ずつ呼び出し元へ渡していく仕組み だとわかります。
検査例外をthrowするときもthrowsが必要
自分で throw new Exception(...) と書いた場合も同じです。検査例外を投げるメソッドには throws が要ります。
public class ThrowChecked {
static void check(String title) {
if (title.isBlank()) {
throw new Exception("タイトルが空です");
}
}
public static void main(String[] args) {
check("");
}
}
ThrowChecked.java:4: error: unreported exception Exception; must be caught or declared to be thrown
throw new Exception("タイトルが空です");
^
1 error
static void check(String title) throws Exception と宣言し、main 側で catch するか main にも throws を付けるとコンパイルできます。throw と throws は対立するものではなく、検査例外ではセットで使うもの です。
throwsに複数の例外を書く
throws の後ろには、クラス名をカンマで区切っていくつでも書けます。受け取る側は、catch を分けたり、| でまとめたりできます。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.concurrent.TimeoutException;
public class MultiThrows {
// throws の後ろにはクラス名をカンマ区切りで複数書ける
static LocalDate readDeadline(String fileName, long waitMillis)
throws IOException, InterruptedException, TimeoutException {
if (waitMillis > 1000) {
throw new TimeoutException("待ち時間が長すぎます: " + waitMillis + "ms");
}
Thread.sleep(waitMillis);
String text = Files.readString(Path.of(fileName)).strip();
return LocalDate.parse(text); // DateTimeParseException は非検査なので throws 不要
}
public static void main(String[] args) throws Exception {
Files.writeString(Path.of("deadline.txt"), "2026-10-31\n");
System.out.println("期限: " + readDeadline("deadline.txt", 10));
try {
readDeadline("deadline.txt", 5000);
} catch (IOException | InterruptedException e) {
System.out.println("入出力か割り込み: " + e);
} catch (TimeoutException e) {
System.out.println("時間切れ: " + e.getMessage());
}
Files.writeString(Path.of("deadline.txt"), "10/31\n");
try {
readDeadline("deadline.txt", 10);
} catch (DateTimeParseException e) {
System.out.println("日付の形式違い: " + e.getMessage());
}
Files.delete(Path.of("deadline.txt"));
}
}
実行結果:
期限: 2026-10-31
時間切れ: 待ち時間が長すぎます: 5000ms
日付の形式違い: Text '10/31' could not be parsed at index 0
LocalDate.parse が投げる DateTimeParseException は非検査例外なので、throws に書いていなくてもコンパイルでき、呼び出し元で catch することもできます。
検査例外と非検査例外の違い|throwsが必要かどうかの分かれ目
「throws を書かないとコンパイルエラーになる例外」と「書かなくてもよい例外」があるのは、Java の例外が2種類に分かれているためです。
| 比べる点 | 検査例外(checked) | 非検査例外(unchecked) |
|---|---|---|
| クラスの系統 | Exception を継承し、RuntimeException を継承していないもの |
RuntimeException とそのサブクラス、Error とそのサブクラス |
| コンパイラの確認 | あり(catch か throws が必須) | なし(どちらも任意) |
| throws の記述 | 必須 | 書いてもよいが省略できる |
| 主な原因 | プログラムの外側の事情(ファイルがない、通信が切れた、待機中に割り込まれた) | プログラムの書き方や渡した値(null、範囲外の添字、形式の違う文字列) |
| 基本の向き合い方 | 起こり得るものとして、対応を用意しておく | 原因になっているコードや入力チェックを見直す |
クラスの親子関係を図にすると次のようになります。
classDiagram
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException
Exception <|-- IOException
Exception <|-- SQLException
Exception <|-- InterruptedException
RuntimeException <|-- IllegalArgumentException
RuntimeException <|-- NullPointerException
RuntimeException <|-- IllegalStateException
IllegalArgumentException <|-- NumberFormatException
Error <|-- OutOfMemoryError
class Error { 非検査 }
class RuntimeException { 非検査 }
class IOException { 検査 }
class SQLException { 検査 }
class InterruptedException { 検査 }
RuntimeException の下か Error の下にあれば非検査、それ以外の Exception の下にあれば検査 です。
検査例外の代表例一覧
下の表の「分類」と「親クラス」は、Java 21 で Class.isAssignableFrom と getSuperclass を使って確かめた結果です。
| 例外クラス | 親クラス | 主に発生する場面 |
|---|---|---|
IOException |
Exception |
ファイルやネットワークの入出力 |
FileNotFoundException |
IOException |
FileInputStream などで開くファイルが見つからない |
NoSuchFileException |
FileSystemException |
Files.readString などで対象のファイルがない |
SQLException |
Exception |
JDBC でのデータベース操作 |
InterruptedException |
Exception |
Thread.sleep などの待機中に割り込まれた |
ClassNotFoundException |
ReflectiveOperationException |
Class.forName で指定したクラスが見つからない |
TimeoutException |
Exception |
Future.get(時間) などで制限時間を超えた |
ExecutionException |
Exception |
別スレッドで実行したタスクの中で例外が起きた |
URISyntaxException |
Exception |
new URI(文字列) の形式が正しくない |
ParseException |
Exception |
SimpleDateFormat.parse などで解析できない |
CloneNotSupportedException |
Exception |
Cloneable を実装していないクラスで clone を呼んだ |
非検査例外の代表例一覧
| 例外クラス | 親クラス | 主に発生する場面 |
|---|---|---|
NullPointerException |
RuntimeException |
null に対してメソッド呼び出しやフィールド参照をした |
IllegalArgumentException |
RuntimeException |
引数の値が受け付けられない範囲だった |
NumberFormatException |
IllegalArgumentException |
Integer.parseInt("abc") のように数値にできない文字列 |
IllegalStateException |
RuntimeException |
オブジェクトがその操作を受け付けない状態だった |
ArrayIndexOutOfBoundsException |
IndexOutOfBoundsException |
配列の範囲外の添字 |
ClassCastException |
RuntimeException |
変換できない型へのキャスト |
ArithmeticException |
RuntimeException |
整数の 0 での割り算 |
UnsupportedOperationException |
RuntimeException |
List.of(...) で作ったリストへの add など |
ConcurrentModificationException |
RuntimeException |
拡張 for 文の途中でコレクションを変更した |
NoSuchElementException |
RuntimeException |
空の Optional への get、要素のない Iterator.next |
DateTimeParseException |
DateTimeException |
LocalDate.parse で形式が合わない |
UncheckedIOException |
RuntimeException |
IOException を非検査例外として包んだもの |
OutOfMemoryError |
VirtualMachineError |
ヒープなどのメモリが足りない(Error 系) |
StackOverflowError |
VirtualMachineError |
再帰呼び出しが深くなりすぎた(Error 系) |
独自例外クラスの作り方|メッセージとcauseを受けるひな形
標準の例外では意味が伝わりにくいとき、アプリ固有の例外クラスを作れます。作り方は Exception(検査例外にしたいとき)か RuntimeException(非検査例外にしたいとき)を継承して、コンストラクタを用意するだけ です。
用意しておくと便利なコンストラクタは次の2つです。
| コンストラクタ | 使う場面 |
|---|---|
(String message) |
自分で条件を判定して投げるとき |
(String message, Throwable cause) |
別の例外を catch して、包んで投げ直すとき |
次のコードは、検査例外 TaskLoadException と非検査例外 InvalidTaskException の2つを定義し、それぞれ「元の例外を cause に入れて投げ直す」使い方を示したものです。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CustomDemo {
static String loadTasks(String fileName) throws TaskLoadException {
try {
return Files.readString(Path.of(fileName));
} catch (IOException e) {
// 元の例外 e を cause として渡して、包んで投げ直す
throw new TaskLoadException("タスク一覧を読み込めませんでした: " + fileName, e);
}
}
static int parsePriority(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
throw new InvalidTaskException("優先度は数字で指定してください: " + text, e);
}
}
public static void main(String[] args) {
try {
loadTasks("tasks.txt");
} catch (TaskLoadException e) {
System.out.println("メッセージ: " + e.getMessage());
System.out.println("原因のクラス: " + e.getCause().getClass().getName());
e.printStackTrace(System.out);
}
System.out.println("----");
parsePriority("high");
}
}
// 独自の検査例外(Exception を継承)
class TaskLoadException extends Exception {
public TaskLoadException(String message) {
super(message);
}
public TaskLoadException(String message, Throwable cause) {
super(message, cause);
}
}
// 独自の非検査例外(RuntimeException を継承)
class InvalidTaskException extends RuntimeException {
public InvalidTaskException(String message) {
super(message);
}
public InvalidTaskException(String message, Throwable cause) {
super(message, cause);
}
}
tasks.txt が存在しない状態で、javac CustomDemo.java のあと java CustomDemo で実行した結果です。
メッセージ: タスク一覧を読み込めませんでした: tasks.txt
原因のクラス: java.nio.file.NoSuchFileException
TaskLoadException: タスク一覧を読み込めませんでした: tasks.txt
at CustomDemo.loadTasks(CustomDemo.java:11)
at CustomDemo.main(CustomDemo.java:25)
Caused by: java.nio.file.NoSuchFileException: tasks.txt
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
at java.base/java.nio.file.Files.newByteChannel(Files.java:380)
at java.base/java.nio.file.Files.newByteChannel(Files.java:432)
at java.base/java.nio.file.Files.readAllBytes(Files.java:3281)
at java.base/java.nio.file.Files.readString(Files.java:3359)
at java.base/java.nio.file.Files.readString(Files.java:3318)
at CustomDemo.loadTasks(CustomDemo.java:8)
... 1 more
----
Exception in thread "main" InvalidTaskException: 優先度は数字で指定してください: high
at CustomDemo.parsePriority(CustomDemo.java:19)
at CustomDemo.main(CustomDemo.java:32)
Caused by: java.lang.NumberFormatException: For input string: "high"
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 CustomDemo.parsePriority(CustomDemo.java:17)
... 1 more
なお、java CustomDemo.java の単一ファイル実行でも動きますが、その場合はスタックトレースに実行ツール自身の行(com.sun.tools.javac.launcher など)が数行加わります。
独自例外のひな形(コピペ用)
クラス名と親クラスを書き換えるだけで使える最小の形です。1クラス1ファイルにする場合は、それぞれ public を付けてクラス名と同じファイル名で保存します。
// 検査例外にするなら Exception、非検査例外にするなら RuntimeException を継承する
public class TaskLoadException extends Exception {
public TaskLoadException(String message) {
super(message);
}
public TaskLoadException(String message, Throwable cause) {
super(message, cause);
}
}
このひな形は上の CustomDemo.java に含めたものと同じ内容です(public を付けた点だけが異なります)。
検査例外と非検査例外、どちらで作るかの判断表
| 状況 | 選ぶ親クラス | 理由 |
|---|---|---|
| 呼び出し元に必ず対応を考えてほしい(再試行する、別の入力を求める など) | Exception |
コンパイラが catch か throws を求めるので、対応の書き漏れに気づける |
| 引数の誤りなど、呼び出し方を直せば起きないもの | RuntimeException |
毎回 try-catch を書かせずに済む |
| ラムダ式や Stream の中から投げたい | RuntimeException |
標準の関数型インターフェースは検査例外を throws に持たないため |
| フレームワークがまとめて処理する層まで届けたい | RuntimeException |
途中のメソッドすべてに throws を書かずに済む |
名前は 末尾を Exception にし、何が起きたかがわかる名前 にすると、スタックトレースの1行目を読むだけで状況が伝わります。
例外を包んで投げ直す|スタックトレースの「Caused by」の読み方
catch した例外を、別の例外の cause に入れて投げ直すことを「例外をラップする(包む)」と呼びます。上の実行結果をもとに、スタックトレースの読み方を整理します。
| 表示 | 意味 |
|---|---|
1行目 TaskLoadException: タスク一覧を… |
最後に投げられた(外側の)例外の型とメッセージ |
at CustomDemo.loadTasks(CustomDemo.java:11) |
外側の例外を new した場所。上の行ほど発生源に近い |
Caused by: java.nio.file.NoSuchFileException: tasks.txt |
cause に入っていた(内側の)元の例外 |
Caused by の下の at ... |
元の例外が発生した場所。CustomDemo.java:8 が自分のコードで最初に関わった行 |
... 1 more |
外側の例外と共通する呼び出し元の行を1行省略した、という意味 |
調べるときは一番下の「Caused by」から読む のが近道です。そこに、最初に起きた例外と発生場所が書かれています。今回の例では、「タスク一覧を読み込めませんでした」という大まかな情報と、「tasks.txt というファイルがなかった」という具体的な原因の両方が1つの出力に残っています。
causeを渡さないと元の情報が残らない
2つ目の引数を渡さずに投げ直すと、どうなるでしょうか。
public class NoCause {
static int parsePriority(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
// cause を渡していない
throw new IllegalArgumentException("優先度は数字で指定してください: " + text);
}
}
public static void main(String[] args) {
parsePriority("high");
}
}
実行結果:
Exception in thread "main" java.lang.IllegalArgumentException: 優先度は数字で指定してください: high
at NoCause.parsePriority(NoCause.java:7)
at NoCause.main(NoCause.java:11)
「Caused by」の行がなくなり、Integer.parseInt のどこで何が起きたのかという情報が出力に残っていません。 直す前と後を並べると次のとおりです。
| コード | スタックトレース | |
|---|---|---|
| 直す前 | throw new IllegalArgumentException("…" + text); |
外側の例外だけ |
| 直した後 | throw new IllegalArgumentException("…" + text, e); |
外側の例外+ Caused by: NumberFormatException |
catch して別の例外を投げるときは、2つ目の引数に元の例外 e を渡す。これだけで、原因を追うための手がかりが残ります。標準の例外クラスの多くは (String, Throwable) のコンストラクタを持っています。
同じ例外をそのまま投げ直す
ログだけ出して、例外そのものは呼び出し元に渡したい場合は、throw e; と書きます。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Rethrow {
static String read(String name) throws IOException {
try {
return Files.readString(Path.of(name));
} catch (IOException e) {
System.out.println("[ログ] 読み込みで例外: " + e.getClass().getSimpleName());
throw e; // 同じ例外をそのまま投げ直す
}
}
// main に throws を付けると、例外は JVM まで届く
public static void main(String[] args) throws IOException {
read("memo.txt");
}
}
実行結果(先頭3行):
[ログ] 読み込みで例外: NoSuchFileException
Exception in thread "main" java.nio.file.NoSuchFileException: memo.txt
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
同じインスタンスを投げ直しているので、「Caused by」は付かず、元のスタックトレースがそのまま表示されます。また、main に throws IOException を付けた場合、例外は JVM まで届き、スタックトレースを表示してプログラムが終了します。
つまずきやすい箇所と実際のエラー文
ラムダ式の中で検査例外が出るメソッドを呼ぶ
forEach や Stream の map に渡すラムダ式の中で、検査例外を投げるメソッドをそのまま呼ぶとコンパイルエラーになります。外側のメソッドに throws を付けても解消しません。
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class LambdaChecked {
public static void main(String[] args) {
List.of("a.txt", "b.txt").forEach(n -> System.out.println(Files.readString(Path.of(n))));
}
}
LambdaChecked.java:6: error: unreported exception IOException; must be caught or declared to be thrown
List.of("a.txt", "b.txt").forEach(n -> System.out.println(Files.readString(Path.of(n))));
^
1 error
forEach が受け取る Consumer の accept メソッドには throws が宣言されていないため、ラムダ式の中から検査例外を外へ出せません。ラムダ式の中で catch し、非検査例外に包んで投げ直す と通ります。IOException 用には、標準で UncheckedIOException が用意されています。
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class LambdaFixed {
public static void main(String[] args) {
try {
List.of("a.txt", "b.txt").forEach(n -> {
try {
System.out.println(Files.readString(Path.of(n)));
} catch (IOException e) {
// 検査例外を非検査例外に包んで投げ直す
throw new UncheckedIOException(e);
}
});
} catch (UncheckedIOException e) {
System.out.println("外側で受け取り: " + e.getMessage());
System.out.println("元の例外: " + e.getCause().getClass().getName());
}
}
}
実行結果(a.txt が存在しない状態):
外側で受け取り: java.nio.file.NoSuchFileException: a.txt
元の例外: java.nio.file.NoSuchFileException
投げられない検査例外をcatchする
try の中で発生し得ない検査例外を catch に書くと、これもコンパイルエラーになります。
import java.io.IOException;
public class NeverThrown {
public static void main(String[] args) {
try {
System.out.println("hello");
} catch (IOException e) {
System.out.println(e);
}
}
}
NeverThrown.java:6: error: exception IOException is never thrown in body of corresponding try statement
} catch (IOException e) {
^
1 error
日本語表示では「例外IOExceptionは対応するtry文の本体ではスローされません」です。リファクタリングで try の中の処理を移したあとによく見かけます。不要になった catch を取り除けば解消します。
オーバーライドでthrowsを広げる
親クラスのメソッドをオーバーライドするとき、親より広い検査例外を throws に書くことはできません。
import java.io.IOException;
class Loader {
void load() throws IOException {}
}
class WideLoader extends Loader {
@Override
void load() throws Exception {}
}
public class OverrideWider {
public static void main(String[] args) {}
}
OverrideWider.java:7: error: load() in WideLoader cannot override load() in Loader
void load() throws Exception {}
^
overridden method does not throw Exception
1 error
Loader 型の変数で load() を呼ぶコードは IOException だけに備えているので、サブクラスがそれより広い例外を投げると約束が崩れるためです。オーバーライドする側で書ける throws を整理すると次のようになります。
| オーバーライド側の throws | 可否 |
|---|---|
親と同じ(throws IOException) |
書ける |
親より狭い(throws FileNotFoundException) |
書ける |
| 何も書かない | 書ける |
非検査例外を追加(throws IllegalStateException) |
書ける |
親より広い・別系統の検査例外(throws Exception) |
コンパイルエラー |
nullをthrowするとNullPointerExceptionになる
変数に入れた例外を投げるとき、その変数が null だと、意図した例外の代わりに NullPointerException が発生します。
public class ThrowNull {
public static void main(String[] args) {
RuntimeException e = null;
throw e;
}
}
Exception in thread "main" java.lang.NullPointerException: Cannot throw exception because "<local1>" is null
at ThrowNull.main(ThrowNull.java:4)
(<local1> は、単一ファイル実行ではローカル変数名の情報が含まれないための表示です。)
throwとthrowsの使い分けチェックリスト
| やりたいこと | 書くもの |
|---|---|
| 引数や状態が正しくないことを知らせたい | throw new IllegalArgumentException("説明: " + 値); |
| 検査例外を自分で処理せず、呼び出し元に任せたい | メソッドに throws 例外クラス名 |
| 検査例外を自分のメソッドで投げたい | throw new XxxException(...) と throws XxxException の両方 |
| 低い層の例外を、意味のわかる例外に置き換えたい | catch (IOException e) { throw new TaskLoadException("説明", e); } |
| ログを出してから同じ例外を渡したい | catch (IOException e) { ログ出力; throw e; } |
| ラムダ式の中の検査例外を外へ出したい | ラムダ式の中で catch し、RuntimeException 系に包んで throw |
メッセージには、「何をしようとして」「どの値で」うまくいかなかったか を入れておくと、スタックトレースの1行目だけで状況がつかめます。本記事の例でも "数量は1以上で指定してください: " + quantity のように、値を一緒に入れています。
よくある質問
Q. mainメソッドに throws Exception を付けてもよいですか?
A. 学習用の短いプログラムや使い捨てのツールでは、コードが短くなるので便利です。例外は JVM まで届き、スタックトレースを表示して終了します(終了コードは 1)。利用者に向けたメッセージを出したり、後始末をしたりする必要があるプログラムでは、main の中で catch して対応を書きます。
Q. throws Exception と書けば全部まとめられますが、それでもよいですか?
A. コンパイルは通ります。ただ、呼び出し元から見ると「何が起きるのか」がわからなくなり、受け取る側も catch (Exception e) と広く受けることになります。throws IOException, TimeoutException のように具体的な型を書くと、呼び出し元が例外ごとに対応を分けられます。
Q. 非検査例外も throws に書いたほうがよいですか?
A. 書かなくてもコンパイルでき、書いても呼び出し元に catch の義務は生じません。「この引数のときにこの例外を投げる」という情報は、Javadoc の @throws タグに書いておくと、呼び出す人に伝わります。
Q. throw と return はどう違いますか? A. どちらもメソッドの処理をそこで終えますが、return は戻り値を1つ上の呼び出し元に渡して通常どおり処理を続けます。throw は、対応する catch が見つかるまで呼び出し元を何段でもさかのぼります。「通常の結果」は return、「続けられない状況」は throw と使い分けます。
まとめ
- throw は例外を投げる文。メソッド本体の中に書き、後ろには
newした例外のインスタンスを1つ置く - throws は例外が出ることの宣言。メソッド宣言の
)の後ろに書き、クラス名をカンマ区切りで複数置ける - 検査例外で throws を書き忘れると
unreported exception ...; must be caught or declared to be thrown。catch するか throws を付けるかのどちらかで解消する RuntimeExceptionとErrorの系統は非検査例外。throws も catch も任意- 独自例外は
(String message)と(String message, Throwable cause)の2つのコンストラクタを用意 すると使いやすい - 包んで投げ直すときは元の例外を cause に渡す。スタックトレースに「Caused by」として原因が残る
- スタックトレースは一番下の「Caused by」から読む と、最初に起きた例外にたどり着ける
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java throwとthrowsの違い|例外の投げ方と独自例外の作り方」
株式会社RJCでは、Javaの例外の投げ方や独自例外の設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。