例外とは
例外とは、Javaが「何かがうまくいかず、通常どおりには続行できない」と伝える方法です。誤った値を返したり、データを静かに壊したりする代わりに、ランタイムは問題を説明する例外オブジェクトを作成し、それをスローします。通常の実行はその時点で止まり、Javaはその状況に対処する方法を知っているコードを探し始めます。
誰もそれを処理しなければ、例外はプログラムの最上位まで到達し、JVMはスタックトレースを出力し、プロセスは非ゼロのステータスで終了します。
"after"が決して出力されないことに注目してください。numbers[5]が評価された瞬間にArrayIndexOutOfBoundsExceptionがスローされ、mainの残りの部分は破棄されます。
スタックトレースを読む
例外が処理されないままになると、次のような出力が得られます。威圧的に見えますが、これは単なるリストにすぎません。
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:8)
at Main.main(Main.java:4)
上から下へ読みます。
- 最初の行は型(
ArithmeticException)とメッセージ(/ by zero)です。 - 各
at ...行はスタックフレームです。一番上は例外が実際にスローされた場所、つまり8行目のdivideです。その下は呼び出し元、4行目のmainです。
一番上のat行が最初に見る場所です。その下のフレームは「どうやってここに来たのか?」という問いに答えます。
これを実行すると、トレースはa / bの行をまっすぐ指し示し、続いて呼び出し元としてmainを表示します。この呼び出しの連鎖は、Javaが無料で提供してくれる最も役立つデバッグツールです。
例外の階層
すべての例外はオブジェクトであり、それらはすべてThrowableから派生します。重要な2つの分岐は次のとおりです。
Error—OutOfMemoryErrorやStackOverflowErrorのように、JVMが発生させる深刻な問題です。これらは通常キャッチしません。Exception— プログラムが合理的に予測して処理できる問題です。その内部にRuntimeExceptionがあり、これはNullPointerExceptionのような日常的なバグの親です。
Throwable
├── Error (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArithmeticException
└── ArrayIndexOutOfBoundsException
これらは実際のクラスであるため、例外はメッセージを持つことができ、自分自身について問い合わせることができます。
getMessage()はスタックトレースのコロンの後ろのテキストを返します。getClass().getSimpleName()は例外の型を名前で返します。
チェック例外と非チェック例外
これは初心者が最もつまずく区別です。
- 非チェック例外は
RuntimeExceptionを継承します。通常はコードのバグを意味します。予期しなかったnull、不正なインデックス、ゼロによる除算などです。コンパイラはそれらの処理を強制しません。 - チェック例外は
Exceptionを継承しますがRuntimeExceptionは継承しません(たとえばIOException)。これは自分の制御外の状況を表します。存在しないファイル、切断されたネットワーク接続などです。コンパイラはそれらをキャッチするか宣言するかを強制します。
メソッドがチェック例外をスローし得る場合、throwsでそれを示さなければならず、すべての呼び出し元がそれに対処しなければなりません。
mainからthrows Exceptionを取り除くと、コードはコンパイルすらされません。これはコンパイラがチェック例外の契約を強制しているのです。非チェック例外がこれを要求することは決してありません。
自分で例外をスローする
例外に反応するだけではありません。自分でそれを発生させることもできます。引数や状態が不正であることを示すには、新しい例外オブジェクトとともにthrowを使います。これは-1のようなマジックな値を返して呼び出し元がチェックしてくれることを願うよりも、はるかに優れています。
何が間違っていたのか、できれば問題のある値も説明するメッセージを常に含めましょう。スタックトレースを読む未来の自分が感謝するはずです。入力を検証するときに最もよく使うのはIllegalArgumentExceptionとIllegalStateExceptionの2つです。
よくある落とし穴
- 例外を握りつぶす。 例外をキャッチして何もしない(空のブロック)と、バグが隠れてしまいます。最低でもログに記録しましょう。通常は処理するか再スローすべきです。
Exceptionを広くキャッチしすぎる。 基底のException(さらに悪い場合はThrowable)をキャッチすると、処理するつもりのなかった問題を覆い隠してしまいます。期待している具体的な型をキャッチしましょう。- トレースを下から上へ読む。 最も関連性の高いフレームは一番上の
at行であって、一番下ではありません。そこから始めましょう。 ErrorとExceptionを混同する。OutOfMemoryErrorやStackOverflowErrorから回復しようとしないでください。代わりに根本原因を修正しましょう。
次: try-catch
これで例外が何であるか、そしてその読み方がわかりました。次のページでは、実際にそれらをどう処理するかを扱います。リスクのあるコードをtryブロックで包み、catchで回復し、finallyでクリーンアップコードを実行することで、プログラムがクラッシュせずに動き続けるようにします。
よくある質問
Javaの例外とは何ですか?
例外とは、プログラムの実行中に検出された問題を表すオブジェクトです。たとえばゼロによる除算、配列の末尾を超えたインデックスアクセス、nullに対するメソッド呼び出しなどです。問題が発生すると、Javaは例外をスローします。つまり通常の流れを止め、それを処理できるコードを探します。何も処理しなければ、プログラムはスタックトレースを出力して終了します。
Javaのチェック例外と非チェック例外の違いは何ですか?
チェック例外(ExceptionのサブクラスだがRuntimeExceptionではないもの、例: IOException)は、キャッチするかthrowsで宣言するかのいずれかが必要で、コンパイラがそれを強制します。非チェック例外(RuntimeExceptionのサブクラス、例: NullPointerException、ArrayIndexOutOfBoundsException)は通常プログラミング上のバグを示し、宣言は不要です。Error(OutOfMemoryErrorなど)も非チェックで、一般にキャッチすることを意図していません。
Javaのスタックトレースはどう読めばよいですか?
上から下へ読みます。最初の行は例外の型とメッセージを示します(例: java.lang.ArithmeticException: / by zero)。その下の各at ...行はスタックフレームで、メソッド、ファイル、行番号を表し、例外がスローされた場所から始まり、呼び出し元をさかのぼっていきます。一番上のat行は、ほぼ常に最初に見るべき場所です。