例外(2)
例外は呼び出し元に戻り値として返されるのではなく、throwされ上位メソッドに伝播されるものでした。
どんな場合でもJava仮想マシン(JVM)に例外処理を任せる事が許されるならば良いのですが、どんな例外でも発生したならばアプリケーションを終了してしまうのでは問題です。
したがって、例外を処理する為の仕組みが必要となります。
例外処理 Exception Handling
throwされた例外(Exception)を処理することは、例外処理(Exception Handling)と呼ばれ、try/catch構文を使い記述します。
try/catch構文を用いる場合、例外が発生する可能性のある処理をtryブロックの中に記述し、続くcatchブロックに例外処理を記述します。
例外が発生せずに全ての処理が実行されれば、catchブロックはスキップされ、後続処理が行われます。
もし、tryブロック内で例外が発生したならば、直ちにtryブロックを抜け、catchブロックの例外処理が実行されます。
try { // 例外が発生する可能性のある処理 } catch(例外の型 e) { // 例外処理 }
例外処理は一種の条件分岐に過ぎません。
例えば、異常コードをメソッドの戻り値として使用するならばエラー処理はif文を使って書くことができます。
// 処理 int resultCode = foo(); if(resultCode == -1) { // 例外処理1 } resultCode = bar(); if(resultCode == -1) { // 例外処理2 } // 後続処理と例外処理が続く…
このように異常コードを使ったエラー処理をJavaで記述することは可能です。
しかし、try/catch構文を使えば、中核となる処理の流れは全てtryブロックに、例外処理はcatchブロックに記述できる為、比べ物にならないほど読みやすいコードとなります。
悪意を持って読みにくいコードを書きたいのでなければ異常コードによる異常系処理は行ってはいけません。
try
tryブロックは必ず先頭に位置し、例外が発生する可能性のある処理を記述します。
この処理はどれだけ長い処理であっても、短い処理であっても構いません。
メソッドを呼び、そのメソッドはさらに別のメソッドを呼び、と非常に深いコードになることもあるでしょう。
しかし、tryブロック内に何百行にも及ぶコードを書くことは避けるべきです。
それは性能的な問題や言語仕様としての問題ではなく、単純に読みにくいからです。
そのような場合はprivateメソッドに処理を記述し、例外処理だけを記述するようにするなど工夫をすると読みやすいコードとなるでしょう。
[修正前]
try { // ***************** // ***************** // ***************** // 非常に長い処理 // ***************** // ***************** // ***************** } catch(例外の型 e) { // 例外処理 }
[修正後]
try { foo(); // foo()では非常に長い処理を行っている } catch(例外の型 e) { // 例外処理 }
catch
catchブロックは例外の宣言と例外処理の記述ブロックの2つで構成されます。
例外の宣言では括弧の中に例外のクラス名(型)と変数名(通常はExceptionのeが多い)を記述し、例外が発生した場合にメソッドのように振舞います。
すなわち、メソッドの引数のようにcatchした例外は変数に代入され、catchブロックでローカル変数として使用可能です。
この時に宣言できる例外の型はtryブロックの中で発生する可能性のある例外に限られます。
発生する可能性のある例外とはメソッドのthrows句で宣言されている例外です。
実装の中でthrowする必ず記述がある必要はありません。
例外はcatchされるとそのメソッドで例外の伝播は止まり、上位メソッドには例外は伝播されません。
したがって、catchして適切な例外処理を行ったならば、そのメソッドでは下位メソッドからの例外が上でthrowされなくなる為、throwsで例外の宣言を行う必要がなくなります。
public void foo() { try { bar(); } catch(AException e) { // 例外処理 } } private void bar() throws AException { // 処理 }
catch可能な例外
catch文に記述可能な例外はtryブロックで発生する可能性のある例外ですが、例外クラスの継承関係を使いスーパークラスでcatchする事も可能です。
極端な事を言えば、全てThrowableでcatchして例外処理を記述することも言語仕様的には許されます。
try { // 処理が1行もなくともコンパイル可能 } catch(Throwable e) { // 例外処理? }
しかし、全ての例外クラスをひとくくりに処理してしまうのは強引です。
ですが、特定の例外クラスとそのサブクラスをまとめて処理する事には活用できるでしょう。
複数のcatchブロック
発生する可能性のある例外が複数ある場合など、catchブロックを複数記述することもできます。
try { // 例外が発生する可能性のある処理 } catch(例外の型1 e) { // 例外処理1 } catch(例外の型2 e) { // 例外処理2 } catch(例外の型3 e) { // 例外処理3 }
このように記述されたcatchブロックは前に記述されたブロックから判定が行われます。
すなわち発生した例外が、例外の型1と等しいかそのサブクラスであった場合には例外処理1が実行されます。
そうでなかった場合で、例外の型2と等しいかそのサブクラスであった場合には例外処理2が実行されます(以下略)。
したがって、次のように記述してしまうと最初のExceptionによるcatchの方が優先的になってしまい、後続のcatchブロックが到達不能コードとなります(コンパイルエラー)。
// AExceptionはExceptionのサブクラス try { // 処理 } catch(Exception e) { // 例外処理 } catch(AException e) { // 例外処理 }
チェック例外 Checked Exception
例外は大きくRuntimeException, Exception(RuntimeException以外), Errorの3種類に分類されました。
この中でRuntimeException以外のExceptionは、チェック例外(検査例外, Checked Exception)と呼ばれます。
チェック例外に対して他の例外をまとめて非チェック例外(Unchecked Exception)と呼びます。
チェック例外は「プログラマーが例外処理(チェック)を行わなければならない例外」と解釈される事が多いかと思います。
しかし、チェック例外は「コンパイラがコンパイル時に例外処理が行われているかチェック(検査)する例外」が正しい理解です。
チェック例外のチェック(検査)では、例外がthrowしているコードまたは例外がthrowされる可能性のあるメソッドを実行した際に、適切にcatchされているかがチェックされます。
catchされていなくともそのメソッドのthrows句で例外を伝播する事が宣言されているならば問題はありません。
ただし、そのメソッドを実行している箇所でも同様のチェックが入る為、結果的にmain()メソッドも含めて何れかの場所でチェックされていなければなりません。
つまり、チェック例外は各メソッドで、throws宣言されているかまたはcatchされていなければなりません。
非チェック例外 Unchecked Exception
非チェック例外(Unchecked Exception)はコンパイル時にチェックされないRuntimeExceptionとErrorの事です。
これらの例外がコンパイル時にチェック(検査)されない理由は、一言で言えば煩雑だからです。
例えば、OutOfMemoryのErrorはメモリが枯渇した場合に発生するErrorですが、このErrorはプログラムのどんな場所でも発生する可能性があります。
また、発生してしまった場合に、問題を回復させアプリケーションを実行し続ける(例外処理を行う)ことは困難です。
そもそも例外処理が正常に動作する保障すらありません。
OutOfMemoryが発生したのであれば、恐らくそれはアプリケーションの設計や実装に問題であり、バグなのです。
また、IllegalArgumentExceptionは不正な引数がメソッドに渡された場合に発生するRuntimeExceptionです。
この例外もアプリケーションの至る所で発生する可能性があります。
しかし、メソッドに不正な引数を渡したのは、メソッド側の問題ではなく実行側の問題ですから、実行側がメソッドを使用する前に引数のチェックを行う責任があります。
それでも不正な引数を渡してしまったならば、それはコーディング上のバグでしかありません。
このようにErrorやRuntimeExceptionが発生する原因は基本的にコーディングミスや設計ミスです。
設計ミスは論外ですが、コーディングミスは充分に注意して実装して充分にテストを行えば、限りなく減らすことができます。
もし、コーディングミスを想定した例外処理を行っていたならば、その例外処理のコーディングミスはどうやって例外処理すれば良いのでしょうか?
また、どのようにテストしたらば良いのでしょうか?
そして、非常に読みににくく解りにくいソースコードができるのではないでしょうか?
全てをチェックした方が良いかもしれません。
しかし、それによって言語自体が使いにくくなってしまっては本末転倒です。
そこで、Javaでは「RuntimeExceptionとErrorは実装者の問題」というスタンスでチェック(検査)を行わないのです。
非チェック例外をcatchする事
try/catch構文では、チェック例外だけではなくErrorやRuntimeExceptionもcatchする事ができます。
しかし、非チェック例外の例外処理は原則としては行うべきではありません。
Errorの例外処理はJava仮想マシンの判断でthrowしているので、原則としては例外処理もJava仮想マシンに一任するべきでしょう。
RutimeExceptionはアプリケーションの判断(例えば引数が不正)でthrowする例外なので、本来はアプリケーションで例外処理を行うべきです。
RuntimeExceptionをアプリケーションでcatchするかはプロジェクトの方針にもよる為、一概には言えません。
1つの傾向としてはRuntimeExceptionを処理するのはフレームワークなどアプリケーションの基盤部分として、それぞれのロジックなどでは例外処理を行わない事です。
RuntimeExceptionは基本的にバグを原因として発生しますが、アプリケーションを止めるわけにも行かない為、main()メソッドに近い基盤部分で一括処理するのがお年どころという事でしょう。
尚、余談ですが最近の傾向としてチェック例外よりも非チェック例外を多く使うという流れがあります。
非チェック例外に統一して例外処理は一括して行うことで、各ロジックには例外処理を埋め込ませない方法です。
実際に開発をしてみた事もありますが、実装者も例外処理をあまり意識しなくて済みメリットとも多く感じました。
しかし、例外処理が必要なところを判断するのが難しくバグを混入させたりとデメリットもあると思えます。
例外処理とチェック例外を理解した上で使うならばともかく経験の浅い技術者に使わせる場合は気をつける必要があるでしょう。
結局のところ、非チェック例外を多く使っても適度にチェック例外は必要で、そのバランスは経験と勘に頼る所なのかもしれません。