はじめに
社内でJava関連の講師として話すことをやめて少し経ちますが、当時例外の話とかをしていて受講生にポカーンとされていたなぁ、とふと思い出したので記録としてまとめてみたのがこの内容です。
初心者向けにしていますが、Javaの文法レベルは理解している人がターゲットになります。
Javaの例外処理とは
基本的には例外はtry 内で throwして、ちゃんとcatchしましょうね、と習うと思います。
try {
// さまざまな処理
throw new Exception("エラーです!");
} catch (Exception e) {
e.printStackTrace();
}
クラスやメソッドをまたがって例外を伝えるには
1つのメソッド内であれば、分かりやすいのですが、メソッドを跨った場合に迷いという名の設計のしどころが出てきます。
public static void main(String[] args) {
try {
call();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void call(){
throw new Exception("メソッド内からのエラーです!");
}
こんな風に書くと、callメソッドの実行行でコンパイルエラーになります。何故だか分かりますでしょうか?
call()メソッド側を
public static void call() throws Exception{
throw new Exception("メソッド内からのエラーです!");
}
もしくは
public static void call() {
throw new RuntimeException("メソッド内からのエラーです!");
}
とするとコンパイルエラーが無くなります。
前者は、このメソッドからExceptionという例外がthrowされる可能性があるよ、ということをメソッドを呼ぶ人に対して
正しく定義するという対処法です。
後者は、RuntimeExceptionというExceptionクラスの継承先クラスですが、Java言語仕様で定められたクラスであり
非チェック例外として、throws節で非チェック例外を宣言する必要がないと定められています。
この仕様はRuntimeExceptionを継承した例外クラスにも継承されます。
非チェック例外と比して、先ほどのExceptionクラスについては、チェック例外と呼びます。
例外をthrowしますよ、ということをメソッドを呼ぶ側と呼ばれる側であらかじめ取り決めておくことを強制することができます。
よって、
public static void main(String[] args) {
call();
}
public static void call() throws Exception{
throw new Exception("メソッド内からのエラーです!");
}
みたいな書き方をすると、mainメソッド側で、callメソッド読んでいるけどtry-catchしていないよ、とコンパイルエラーがでます。
ちなみに
public static void main(String[] args) {
try {
call();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void call() throws RuntimeException{
throw new RuntimeException("メソッド内からのエラーです!");
}
のように、非チェック例外をthrows節に書くことも可能です。
他の例外系のJava標準クラス
ざっくり全体をクラス図で示すと以下のような形になります。
Throwlableクラスが例外処理系のクラスの大元です。名前の通り、throwできるクラスを意味します。
そこから2つに分かれます。ExceptionとErrorです。
Exceptionは前述の通りです。
初登場のErrorですが、エラーを示しますのでこれが起きたらメモリ内の状態異常などが発生している可能性もあるので、Javaプロセスごと再起動したほうが良いレベルです。アプリケーション内でやれることとしては何が起きているのか適切にエラーの内容をログに出力しておくこと、ぐらいです。(ログ出力時のIOErrorであれば、それもままならない状況かもしれませんが、やれることはやりましょう)
例外設計について
さて、話を戻しますと、例外をthrowする際に(特に独自例外クラスを作る場合に)、チェック例外としてthrowしてメソッドの呼び元に例外処理を強制させるのか、非チェック例外にして例外処理をするかどうかを任せるのか?ということを論じます。
前者の場合は、品質は高くなりそうですが、コーディング量は増えます。クラスが増えてきて構造が深くなると、例外をcatchして上位にthrowするだけのコードが量産されてしまいそうです。
(昔ながらの大規模システムでは、こちらが多いような感触はあります)
後者の場合は、コーディング量は適切になりそうですが、適切な例外処理がされているか、しないことを意思を持ってやっているのかはコードレビューや結合テストなどで確認する必要が出てきます。
で、落とし所ですが、非チェック例外を扱いつつ、メソッド定義のthrows節に書くという方法がよいのではないか、と今時点では考えてます。(システム特性、開発体制によるので、あくまで個人意見として受け取ってください)
メソッド呼ぶ側に例外throwするかもしれないよ、ということを示しつつ、強制はされないためコード量が無駄に増えないためです。
public static void main(String[] args) {
try {
call();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void call() throws RuntimeException{
throw new RuntimeException("メソッド内からのエラーです!");
}
このパターンですね。
まとめ
Javaにおける例外処理と考え方について、簡単にまとめました。
ほんのさわりの部分だけですが、設計や実装の奥深さの入り口ぐらいは認識できたのではないでしょうか?
もっとよい、モダンな考え方とかあれば教えてください。
ラムダ関数などFunction系での考え方については、あまり理解していないので、そのあたりも今後理解を深めていきたいです。