- JAVA
Java 独自例外の作り方|extends Exceptionと原因のラップ
Javaの独自例外(カスタム例外)とは、Exception または RuntimeException を継承して自分で定義する例外クラスのことで、public class 名前Exception extends Exception { ... } と書き、コンストラクタで super(message, cause) を呼べば作れます。 「在庫が足りない」「数量を読み取れない」のように、アプリケーション固有の出来事に名前を付けられるのが利点です。
この記事では、4種類のコンストラクタをそろえたひな形を Exception 版と RuntimeException 版の両方で用意しました。さらに、原因(cause)を渡した場合と渡さなかった場合のスタックトレースを並べ、Caused by が表示される様子と消える様子を再現します。エラーコードを持つ例外、getCause でたどる例、どちらを継承するかの判断表まで、すべて実行結果つきで紹介します。
確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。各ファイルを同じフォルダに保存し、javac -d classes *.java でコンパイル、java -cp classes クラス名 で実行しています。
独自例外の早見表
作り方の手順
| 手順 | すること | 書き方 |
|---|---|---|
| 1 | 親クラスを決めて継承する | extends Exception(検査例外)または extends RuntimeException(非検査例外) |
| 2 | クラス名の末尾を Exception にする | StockShortageException など、出来事がわかる名前 |
| 3 | コンストラクタを用意する | super() / super(message) / super(message, cause) / super(cause) |
| 4 | serialVersionUID を書く |
private static final long serialVersionUID = 1L; |
| 5 | 投げる | throw new StockShortageException("在庫が足りません"); |
| 6 | 別の例外を包むときは原因を渡す | throw new StockShortageException("...", e); |
4種類のコンストラクタと、getMessage・getCause の結果
| コンストラクタ | 用途 | getMessage() | getCause() |
|---|---|---|---|
() |
メッセージなしで投げる | null |
null |
(String message) |
自分で見つけた問題を知らせる | 渡したメッセージ | null |
(String message, Throwable cause) |
別の例外を包む(おすすめ) | 渡したメッセージ | 渡した例外 |
(Throwable cause) |
メッセージを付けずに包む | 原因の toString() の結果 |
渡した例外 |
どちらを継承するかの目安
| 継承するクラス | 種類 | throws や try-catch | 向いている場面 |
|---|---|---|---|
Exception |
検査例外 | 必須(ないとコンパイルエラー) | 呼び出し側に対応を必ず書いてほしいとき |
RuntimeException |
非検査例外 | 任意 | 呼び出し側ではその場で対応しにくく、上の層でまとめて扱うとき |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java 独自例外の作り方|extends Exceptionと原因のラップ」
コピペで使える独自例外のひな形
Exception 版(検査例外)
4種類のコンストラクタをそろえた形です。クラス名と説明のコメントを書き換えれば、そのまま使えます。
/** 在庫が足りないことを表す独自例外(検査例外) */
public class StockShortageException extends Exception {
private static final long serialVersionUID = 1L;
/** 1. 引数なし */
public StockShortageException() {
super();
}
/** 2. メッセージ */
public StockShortageException(String message) {
super(message);
}
/** 3. メッセージと原因 */
public StockShortageException(String message, Throwable cause) {
super(message, cause);
}
/** 4. 原因だけ */
public StockShortageException(Throwable cause) {
super(cause);
}
}
RuntimeException 版(非検査例外)
違いは extends の後ろだけです。
/** 注文の内容が正しくないことを表す独自例外(非検査例外) */
public class InvalidOrderException extends RuntimeException {
private static final long serialVersionUID = 1L;
/** 1. 引数なし */
public InvalidOrderException() {
super();
}
/** 2. メッセージ */
public InvalidOrderException(String message) {
super(message);
}
/** 3. メッセージと原因 */
public InvalidOrderException(String message, Throwable cause) {
super(message, cause);
}
/** 4. 原因だけ */
public InvalidOrderException(Throwable cause) {
super(cause);
}
}
ひな形を使ってみる
2つの例外を投げて受け取り、4種類のコンストラクタで getMessage() と getCause() がどうなるかを確かめます。
public class TemplateDemo {
// 検査例外を投げるメソッドは throws が必要
static void reserve(int stock, int quantity) throws StockShortageException {
if (quantity > stock) {
throw new StockShortageException("在庫が足りません: 在庫=" + stock + ", 注文数=" + quantity);
}
System.out.println("引き当て完了: " + quantity + "個");
}
// 非検査例外を投げるメソッドは throws が不要
static void validate(int quantity) {
if (quantity <= 0) {
throw new InvalidOrderException("注文数は1以上で指定してください: " + quantity);
}
}
public static void main(String[] args) {
try {
reserve(10, 3);
reserve(10, 30);
} catch (StockShortageException e) {
System.out.println("catch: " + e.getMessage());
}
try {
validate(0);
} catch (InvalidOrderException e) {
System.out.println("catch: " + e.getMessage());
}
// 4種類のコンストラクタで getMessage と getCause がどうなるか
Throwable cause = new IllegalStateException("元の例外");
StockShortageException[] list = {
new StockShortageException(),
new StockShortageException("メッセージ"),
new StockShortageException("メッセージ", cause),
new StockShortageException(cause),
};
for (StockShortageException e : list) {
System.out.println("getMessage=" + e.getMessage() + " / getCause=" + e.getCause());
}
}
}
実行結果です。
引き当て完了: 3個
catch: 在庫が足りません: 在庫=10, 注文数=30
catch: 注文数は1以上で指定してください: 0
getMessage=null / getCause=null
getMessage=メッセージ / getCause=null
getMessage=メッセージ / getCause=java.lang.IllegalStateException: 元の例外
getMessage=java.lang.IllegalStateException: 元の例外 / getCause=java.lang.IllegalStateException: 元の例外
最後の4行が、コンストラクタごとの違いです。
- 引数なしは、メッセージも原因も
nullです。 - 原因だけを渡すと、メッセージには原因の
toString()の結果(クラス名とメッセージ)が入ります。 Java 21 の API ドキュメントにも、Throwable(Throwable cause)の詳細メッセージは(cause==null ? null : cause.toString())になると書かれています。 - メッセージと原因の両方を渡す形では、原因のメッセージは自動では取り込まれません。必要な情報は、自分のメッセージに書きます。
4つすべて必要か
コンストラクタは親クラスから引き継がれないため、使いたい形は自分で書きます。必ず4つそろえる必要はありません。
| 用意するコンストラクタ | 判断 |
|---|---|
(String message) と (String message, Throwable cause) |
この2つがあれば、ほとんどの場面をカバーできる |
(Throwable cause) |
メッセージを付け足す必要がないときに便利 |
() |
メッセージなしで投げる場面がなければ省いてよい |
検査例外と非検査例外の違いを動かして確かめる
Exception を継承した独自例外は検査例外になり、投げるメソッドに throws を書くか、try-catch で受けるかのどちらかが必要です。どちらもないとコンパイルが止まります。
public class NoThrows {
static void reserve(int stock, int quantity) {
if (quantity > stock) {
throw new StockShortageException("在庫が足りません");
}
}
public static void main(String[] args) {
reserve(10, 30);
}
}
NoThrows.java:5: error: unreported exception StockShortageException; must be caught or declared to be thrown
throw new StockShortageException("在庫が足りません");
^
1 error
メソッドの宣言を static void reserve(int stock, int quantity) throws StockShortageException に直し、呼び出す側を try-catch で囲むと、先ほどの TemplateDemo のとおり動きます。RuntimeException を継承した InvalidOrderException は、throws がなくてもコンパイルできます。
どちらを継承するかの判断表
| 判断の観点 | Exception(検査例外) | RuntimeException(非検査例外) |
|---|---|---|
| 呼び出し側がその場で対応できるか | できる(別の候補を出す、再入力を求めるなど) | しにくい(設定の誤り、プログラムの誤りなど) |
| コンパイラに対応漏れを見つけてほしいか | はい | いいえ |
| ラムダ式の中で投げるか | forEach のラムダ式の中で投げると unreported exception のコンパイルエラーになる |
そのまま投げられる |
| メソッドの宣言 | throws が増える |
変わらない |
| 途中の層を素通りさせたいか | 各層に throws が必要 |
何も書かなくてよい |
迷ったときの目安は次のとおりです。
- 「呼び出し側が必ず対応を書くべき出来事」なら
Exception。 - 「上の層でまとめてログに出し、利用者に知らせる」形なら
RuntimeException。 - チームやフレームワークに決まりがあれば、それに合わせるのがいちばんです。
原因(cause)をラップする
下の層で起きた例外を、意味のわかる独自例外に包み直して投げることを「ラップ」と呼びます。このとき、コンストラクタの第2引数に元の例外を渡すかどうかで、スタックトレースに残る情報が大きく変わります。
まず、2つの引数を持つ独自例外を用意します。
/** 数量の文字列を読み取れないことを表す独自例外 */
public class QuantityParseException extends RuntimeException {
private static final long serialVersionUID = 1L;
public QuantityParseException(String message) {
super(message);
}
public QuantityParseException(String message, Throwable cause) {
super(message, cause);
}
}
cause を渡した場合
public class WithCause {
static int parseQuantity(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
// 第2引数に元の例外を渡す
throw new QuantityParseException("数量を読み取れません: " + text, e);
}
}
public static void main(String[] args) {
int quantity = parseQuantity("12個");
System.out.println(quantity);
}
}
Exception in thread "main" QuantityParseException: 数量を読み取れません: 12個
at WithCause.parseQuantity(WithCause.java:8)
at WithCause.main(WithCause.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 WithCause.parseQuantity(WithCause.java:5)
... 1 more
cause を渡さなかった場合
違いは throw の行だけです。
public class WithoutCause {
static int parseQuantity(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
// 元の例外を渡していない
throw new QuantityParseException("数量を読み取れません: " + text);
}
}
public static void main(String[] args) {
int quantity = parseQuantity("12個");
System.out.println(quantity);
}
}
Exception in thread "main" QuantityParseException: 数量を読み取れません: 12個
at WithoutCause.parseQuantity(WithoutCause.java:8)
at WithoutCause.main(WithoutCause.java:13)
Caused by: 以下がまるごと消えました。 残っているのは「8行目で独自例外を作った」という情報だけで、もとの NumberFormatException が Integer.parseInt のどこで起きたのかは、もう追えません。
| 比べる点 | cause を渡した | cause を渡さなかった |
|---|---|---|
Caused by: の表示 |
あり | なし |
| 元の例外のクラス名 | java.lang.NumberFormatException とわかる |
わからない |
| 元の例外が起きた行 | WithCause.java:5 とわかる |
わからない |
getCause() |
元の例外 | null |
Caused by の読み方
| 表示 | 意味 |
|---|---|
1行目(Exception in thread "main" QuantityParseException: ...) |
最後に投げられた例外(いちばん外側) |
その下の at ... |
その例外が作られた場所。上の行ほど新しい呼び出し |
Caused by: ... |
その例外の原因になった例外。下にある Caused by ほど根本の原因に近い |
... 1 more |
残りの行が、外側の例外のスタックトレースの下から1行分と同じなので省略した、という印 |
API ドキュメントでも、... を含む行は「残りの部分が、この例外を原因に持つ外側の例外のスタックトレースの下から数えた行数分と一致する」ことを示すと説明されています。調査のときは、いちばん下の Caused by から読むと、最初に起きた出来事にたどり着けます。
ラップするときの書き方(直す前と後)
| 直す前 | 直した後 | 理由 |
|---|---|---|
throw new XxxException("読み取れません"); |
throw new XxxException("読み取れません", e); |
原因が残る |
throw new XxxException(e.getMessage()); |
throw new XxxException("数量を読み取れません: " + text, e); |
メッセージだけでは元のクラス名と行がわからない |
e.printStackTrace(); throw new XxxException("..."); |
throw new XxxException("...", e); |
原因を渡しておけば、出力は受け取る側の1か所にまとめられる |
getCause で原因をたどる
例外が何段にも包まれているとき、getCause() を繰り返し呼ぶと1段ずつさかのぼれます。3段に重なった例で確かめます。
public class CauseChain {
static void load() {
try {
Integer.parseInt("12個");
} catch (NumberFormatException e) {
throw new QuantityParseException("数量を読み取れません", e);
}
}
static void process() throws StockShortageException {
try {
load();
} catch (QuantityParseException e) {
throw new StockShortageException("在庫の確認を完了できません", e);
}
}
/** いちばん奥の原因を返す */
static Throwable rootCause(Throwable t) {
Throwable current = t;
while (current.getCause() != null) {
current = current.getCause();
}
return current;
}
public static void main(String[] args) {
try {
process();
} catch (StockShortageException e) {
// getCause で1段ずつたどる
int depth = 0;
for (Throwable t = e; t != null; t = t.getCause()) {
System.out.println(depth++ + ": " + t.getClass().getName() + " / " + t.getMessage());
}
System.out.println("根本の原因: " + rootCause(e));
// 原因の型で分ける
if (e.getCause() instanceof QuantityParseException q) {
System.out.println("原因は数量の読み取り: " + q.getMessage());
}
System.out.println("--- printStackTrace ---");
e.printStackTrace(System.out);
}
}
}
0: StockShortageException / 在庫の確認を完了できません
1: QuantityParseException / 数量を読み取れません
2: java.lang.NumberFormatException / For input string: "12個"
根本の原因: java.lang.NumberFormatException: For input string: "12個"
原因は数量の読み取り: 数量を読み取れません
--- printStackTrace ---
StockShortageException: 在庫の確認を完了できません
at CauseChain.process(CauseChain.java:15)
at CauseChain.main(CauseChain.java:30)
Caused by: QuantityParseException: 数量を読み取れません
at CauseChain.load(CauseChain.java:7)
at CauseChain.process(CauseChain.java:13)
... 1 more
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 CauseChain.load(CauseChain.java:5)
... 2 more
for (Throwable t = e; t != null; t = t.getCause())の形で、外側から順にたどれます。rootCauseは、getCause()がnullになるまで進んで、いちばん奥の例外を返します。e.getCause() instanceof QuantityParseException qのように、原因の型で処理を分けることもできます。printStackTraceではCaused byが2つ並び、下のものほど根本の原因です。... 2 moreは、外側と同じ2行が省略されたことを表します。
エラーコードを持つ独自例外
例外にフィールドを足せば、メッセージ以外の情報も運べます。よく使われるのがエラーコードです。コードの一覧を enum にしておくと、打ち間違いをコンパイル時に見つけられ、switch で漏れなく分けられます。
/** エラーコードの一覧 */
public enum ErrorCode {
STOCK_SHORTAGE("E1001", "在庫が足りません"),
ITEM_NOT_FOUND("E1002", "商品が見つかりません"),
INVALID_QUANTITY("E2001", "数量の指定が正しくありません"),
UNEXPECTED("E9999", "予期しないエラーです");
private final String code;
private final String defaultMessage;
ErrorCode(String code, String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
public String code() {
return code;
}
public String defaultMessage() {
return defaultMessage;
}
}
/** エラーコードを持つ独自例外 */
public class AppException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final ErrorCode errorCode;
public AppException(ErrorCode errorCode) {
super(errorCode.defaultMessage());
this.errorCode = errorCode;
}
public AppException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public AppException(ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
/** ログに出したときにコードも見えるようにする */
@Override
public String toString() {
return getClass().getName() + "[" + errorCode.code() + "]: " + getMessage();
}
}
使う側のコードです。
import java.util.Map;
public class ErrorCodeDemo {
static final Map<String, Integer> STOCK = Map.of("A-100", 5, "B-200", 0);
static void order(String itemId, String quantityText) {
int quantity;
try {
quantity = Integer.parseInt(quantityText);
} catch (NumberFormatException e) {
throw new AppException(ErrorCode.INVALID_QUANTITY, "数量を読み取れません: " + quantityText, e);
}
Integer stock = STOCK.get(itemId);
if (stock == null) {
throw new AppException(ErrorCode.ITEM_NOT_FOUND, "商品が見つかりません: " + itemId);
}
if (quantity > stock) {
throw new AppException(ErrorCode.STOCK_SHORTAGE);
}
System.out.println("注文を受け付けました: " + itemId + " x " + quantity);
}
public static void main(String[] args) {
String[][] orders = { {"A-100", "2"}, {"A-100", "9"}, {"Z-999", "1"}, {"B-200", "二"} };
for (String[] o : orders) {
try {
order(o[0], o[1]);
} catch (AppException e) {
// コードで処理を分ける
String action = switch (e.getErrorCode()) {
case STOCK_SHORTAGE -> "入荷待ちを案内";
case ITEM_NOT_FOUND, INVALID_QUANTITY -> "入力の見直しを案内";
case UNEXPECTED -> "担当者に連絡";
};
System.out.println(e.getErrorCode().code() + " / " + e.getMessage()
+ " / 対応: " + action + " / cause=" + e.getCause());
}
}
System.out.println(new AppException(ErrorCode.UNEXPECTED));
}
}
注文を受け付けました: A-100 x 2
E1001 / 在庫が足りません / 対応: 入荷待ちを案内 / cause=null
E1002 / 商品が見つかりません: Z-999 / 対応: 入力の見直しを案内 / cause=null
E2001 / 数量を読み取れません: 二 / 対応: 入力の見直しを案内 / cause=java.lang.NumberFormatException: For input string: "二"
AppException[E9999]: 予期しないエラーです
e.getErrorCode()で受け取り側の処理を分けられます。 メッセージの文字列を見て判断するより、文言の変更に強くなります。- コードごとの既定のメッセージを enum に持たせたので、
new AppException(ErrorCode.STOCK_SHORTAGE)だけでもメッセージが入ります。 - 原因つきのコンストラクタを用意したので、数量の変換エラーは
causeにNumberFormatExceptionが残っています。 toString()を上書きしたので、最後の行のようにログにコードが出ます。
| 持たせる情報 | 例 | 持たせ方 |
|---|---|---|
| エラーコード | E1001 | enum のフィールド |
| 対象の ID | 商品 ID、注文番号 | private final のフィールドとゲッター |
| 数値 | 在庫数、注文数 | 同上 |
フィールドは final にしてコンストラクタで設定すると、作った後に値が変わらず、扱いやすくなります。
serialVersionUID を書く理由
Throwable は Serializable(オブジェクトをバイト列に変換できる印)を実装しているため、独自例外も直列化できるクラスになります。serialVersionUID を書かないクラスを javac -Xlint:serial でコンパイルすると、次の警告が出ます。
public class NoSerialException extends Exception {
public NoSerialException(String message) {
super(message);
}
}
NoSerialException.java:1: warning: [serial] serializable class NoSerialException has no definition of serialVersionUID
public class NoSerialException extends Exception {
^
1 warning
private static final long serialVersionUID = 1L; を1行足すと、この警告は出なくなります。ひな形に入れてあるのはそのためです。
catch の順番で出るコンパイルエラー
独自例外は親クラスの型でも受け取れます。そのため、親クラスの catch を先に書くと、後ろの catch には届かなくなり、コンパイルが止まります。
public class CatchOrder {
static void reserve() throws StockShortageException {
throw new StockShortageException("在庫が足りません");
}
public static void main(String[] args) {
try {
reserve();
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
} catch (StockShortageException e) {
System.out.println("StockShortageException: " + e.getMessage());
}
}
}
CatchOrder.java:12: error: exception StockShortageException has already been caught
} catch (StockShortageException e) {
^
1 error
catch は「子クラス(独自例外)を先、親クラス(Exception)を後」に並べます。
応用:スタックトレースなしの例外と initCause
スタックトレースを記録しない軽い例外
Throwable には、スタックトレースを記録するかどうかを選べる4引数の protected コンストラクタがあります(Java 7 で追加)。入力チェックのように、発生した場所よりも内容が大切な例外で使えます。
/** スタックトレースを記録しない軽い独自例外 */
public class LightException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LightException(String message) {
// 第3引数: suppressed を使うか / 第4引数: スタックトレースを記録するか
super(message, null, false, false);
}
public static void main(String[] args) {
try {
throw new LightException("入力が空です");
} catch (LightException e) {
System.out.println("getMessage: " + e.getMessage());
System.out.println("スタックトレースの行数: " + e.getStackTrace().length);
e.printStackTrace(System.out);
}
}
}
getMessage: 入力が空です
スタックトレースの行数: 0
LightException: 入力が空です
at ... の行が1つも出ていません。調査の手がかりが減るので、使う場面を絞るのがおすすめです。
原因を後から設定する initCause
原因を受け取るコンストラクタがない例外には、initCause で後から原因を設定できます。
public class InitCauseDemo {
/** メッセージのコンストラクタしかない例外 */
static class SimpleException extends RuntimeException {
private static final long serialVersionUID = 1L;
SimpleException(String message) {
super(message);
}
}
public static void main(String[] args) {
try {
Integer.parseInt("x");
} catch (NumberFormatException e) {
SimpleException wrapped = new SimpleException("変換できません");
wrapped.initCause(e); // 後から原因を設定する
System.out.println("getCause: " + wrapped.getCause());
try {
wrapped.initCause(e); // 2回目
} catch (IllegalStateException ex) {
System.out.println(ex);
}
}
}
}
getCause: java.lang.NumberFormatException: For input string: "x"
java.lang.IllegalStateException: Can't overwrite cause with java.lang.NumberFormatException: For input string: "x"
initCause を呼べるのは1回だけで、2回目は IllegalStateException になります。自分で作る例外なら、原因つきのコンストラクタを用意するほうが簡潔です。
つまずきやすい点と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
unreported exception XxxException; must be caught or declared to be thrown |
検査例外を投げるのに throws も try-catch もない |
メソッドに throws を書くか、try-catch で受ける |
exception XxxException has already been caught |
親クラスの catch が先にある | 子クラスの catch を先に書く |
Caused by が出ず、元の例外がわからない |
ラップするときに cause を渡していない | new XxxException(message, e) の形にする |
[serial] serializable class ... has no definition of serialVersionUID |
serialVersionUID がない |
private static final long serialVersionUID = 1L; を足す |
constructor XxxException in class XxxException cannot be applied to given types; |
その引数のコンストラクタを定義していない | (String, Throwable) のコンストラクタを足す |
getMessage() が原因のクラス名つきの文字列になる |
(Throwable cause) のコンストラクタを使った |
メッセージも渡す形にする |
Can't overwrite cause with ... |
initCause を2回呼んだ、またはコンストラクタで原因を設定済み |
原因はコンストラクタで1回だけ渡す |
独自例外を作るときのチェックリスト
| 確認すること | 目安 |
|---|---|
| 標準の例外で足りないか | 引数の誤りは IllegalArgumentException、状態の誤りは IllegalStateException で表せることが多い |
| クラス名 | 末尾を Exception にし、出来事がわかる名前にする |
| 親クラス | 判断表で Exception か RuntimeException を選ぶ |
| コンストラクタ | (String) と (String, Throwable) は用意する |
| メッセージ | 何が、どの値で起きたかを書く(例:在庫=10, 注文数=30) |
| ラップ | 必ず cause を渡す |
| 種類を増やしすぎない | 受け取り側で処理を分けたい単位で作る。細かい区別はエラーコードで表す |
よくある質問
Q. extends Exception と extends RuntimeException は、どちらを選べばよいですか?
A. 呼び出し側に対応を必ず書いてほしいなら Exception、上の層でまとめて扱うなら RuntimeException です。Exception を継承すると throws か try-catch がないとコンパイルエラーになり、対応漏れに気づけます。RuntimeException はメソッドの宣言を変えずに投げられます。
Q. コンストラクタは4種類すべて必要ですか?
A. 必須ではありません。(String message) と (String message, Throwable cause) の2つがあれば、多くの場面で足ります。原因を渡せる形を1つは用意しておくと、ラップするときに Caused by を残せます。
Q. Caused by が表示されないのはなぜですか?
A. 独自例外を作るときに、元の例外をコンストラクタに渡していないためです。throw new XxxException("メッセージ", e); のように第2引数で渡し、独自例外のコンストラクタでも super(message, cause) を呼びます。
Q. 独自例外にフィールドを追加してもよいですか?
A. 追加できます。エラーコードや対象の ID を private final のフィールドに持たせ、ゲッターで取り出す形が扱いやすいです。受け取り側は、メッセージの文字列ではなくフィールドの値で処理を分けられます。
まとめ
- 独自例外は
extends Exception(検査例外)かextends RuntimeException(非検査例外)で作る。 クラス名の末尾は Exception。 - コンストラクタは
()・(String)・(String, Throwable)・(Throwable)の4種類。 最低でも(String)と(String, Throwable)を用意する。 - ラップするときは必ず cause を渡す。 渡さないと
Caused byが消え、元の例外のクラス名も行も追えなくなる。 - スタックトレースは、いちばん下の
Caused byから読むと根本の原因にたどり着ける。 getCause()を繰り返すと、包まれた例外を1段ずつたどれる。- エラーコードは enum のフィールドとして持たせると、受け取り側が switch で分けやすい。
serialVersionUIDを書き、catch は子クラスを先に並べる。
参考・出典
- Java SE 21 API ドキュメント:java.lang.Throwable(https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java 独自例外の作り方|extends Exceptionと原因のラップ」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。