Javaについて徹底解説!

Javaのtry-catch文を基本から! より良いエラー処理の方法を身に着けよう

Javaのtry-catch文は、プログラム中で例外が発生するか試して(try)、例外が発生したら捕まえて(catch)、何かしらの処理を行いたい場合に使います。

この記事ではtryの用途の一つ「例外処理のtry-catch」を「Javaのエラー処理は良くわからないなぁ」という人向けに解説します。tryが関係するtry-finally文やtry-with-resources文にも少しだけ触れます。

この記事をお読みになる方としては、Javaの例外について一応の知識があることを想定しています。もし自信がない場合は、お持ちのテキストやWEBの記事であらかじめ予習をしておいてください。

※この記事のサンプルは、Java 10の環境で動作確認しています。

1.try-catch文は例外にその場で対処する時に使う

一般的なプログラミング言語で言うエラーは、Javaでは「例外(Exception)」で表現されます。Javaでは、例外が発生しうる箇所で、何かしらの対処を必ず行わなければコンパイルエラーとなってしまいます。

そんな例外に対処するには、以下の二つの方法があります。

  1. その場でエラーへ対処する→try-catch文を使う
  2. その場では例外へは対処せず、処理を中断して呼び出し元に任せるメソッドのthrows節を使う

でも、throws節を使って例外への対処を呼び出し元のメソッドに任せても、結局そちらで同じ二択を迫られます。ですから、Javaではどこかで誰かが必ず例外へ対処するようになっている、ということです。

このように、Javaのエラー処理の特徴は、発生しうる例外への対処を構文上で強制されることです。C言語などでは関数の戻り値や広域変数でエラーの発生有無をチェックしますが、チェックはあくまで任意なので記述し忘れることがあります。例外は、そんな忘れがちなエラー処理を構文上で必須にしたものです。

また、メソッドの戻り値はメソッドが本来戻すべきモノだけに単純化し、エラーは例外として別のオブジェクトで表現する、というプログラミングスタイルでもあります。戻り値に処理結果とエラー有無が組み合わさった形式だと、呼び出し側で結果を解釈する処理が別に必要となってしまうのです。

2.try-catch文の書き方・使い方

2-1.try-catch文の基本構文

try-catch文はtryブロックとcatchブロックを繋げたものです。ブロックとは{}で囲まれた範囲を指すプログラミングでの用語です。

tryブロックでは例外が発生しうる処理を書きます。もしtryブロック内で例外が発生したなら、Javaがその例外を捕まえて、catchブロックに処理を移します。catchブロック内には補足した例外への処理を書いておきます。

catchブロックの変数宣言部分では、ThrowableあるいはThrowableを継承したクラス・インターフェイスだけを型に指定できます。普通のクラスを指定してもコンパイルエラーになります。catchブロックは例外を処理するためにある、例外専用のブロックだからです。

例として、ファイルの読み込み時に発生する例外へ対処してみます。java.io.FileReaderのコンストラクタでは例外FileNotFoundExceptionが発生しうると、コンストラクタのthrows節で示されています。ですので、これにtry-catch文で対処するなら以下となります。

ちなみに、例外の変数名はプログラマーの好きにできますが、単に“e”とするのが広く見られるスタイルで、JDKのソースコードでも同じです。eの他にはexも良く見ますが、exceptionのような長い変数名はほとんど見かけません。明確なものへは単純なものにするのがプログラマーの共通見解です。

2-2.例外が発生すると処理は中断される

例外が発生した時点で、tryブロック内の処理は即座に中断されてcatchブロックに処理が移ります。

つまり、以下のプログラムでは、処理の実行順序は①→②→④→⑤となり、は実行されません。例外が途中で発生しなければ①→②→③→⑤となり、catchブロックの内容(④)は実行されません。

このように本来行うはずだった処理の途中で中断されます。ですから、例えばtryブロック内で開いたファイルを閉じるなどの必要な後処理を忘れないようにしなければなりません。そうしないと、いわゆるリソースリークやメモリリークが発生しえます。

一般的にはcatchブロックの中ではリソースの解放処理は行いません。リソースの解放は正常終了した時にも行う必要があるものですが、catchブロックの中で書いてしまっては正常終了時のリソース解放を書き忘れる恐れがあるからです。

そのために、finallyブロックやtry-with-resources文を用いて、処理の正常終了・例外発生に関わらず、リソース解放をします。以下に簡単にですが例を示します。try-with-resources文では、java.lang.AutoCloseableを実装したクラスなら、tryが終わった際に必ずAutoCloseable.close()を実行してくれます。

2-3.複数の例外をcatchしたい時は

try-catch文のcatchブロックへは、catchしたい例外をいくらでも繋げられます。ただし後述する特定の条件を満たすことが必要で、さらに同じ例外は異なるcatchブロックで複数繋げられません。

例として、FileReaderのメソッドread()でやってみます。read()は、IOExceptionをthrowします。ですので、FileReaderをnewしてreadする際は、FileNotFoundExceptionとIOExceptionの2つの例外を捕捉しなければなりません。

ここで、tryブロック中で発生しうるけれどもcatchされない例外があるなら、そのtry-catch文の外で、どこかで誰かに対処してもらわなければなりません。そうしないとコンパイルエラーを解消できません。

さて、複数の例外を対象としたtry-catch文は、tryブロック内で発生した例外へのswitch文のようなものだと考えると分かりやすいでしょう。具体的には、以下の例でお分かり頂けるでしょうか。

2-3-1.例外の継承関係にはご注意を

ちなみに、前節冒頭の「特定の条件を満たす限り」は、catchしたい複数の例外が継承関係にある場合に現れます。

例外の継承関係で上位にあるものが先にcatchに出てくると、その後のcatchでは下位のクラスを指定できません。そうした場合はコンパイルエラーになってしまいます。

例えば、NullPointerExceptionはRuntimeExceptionのサブクラスですので、RuntimeExceptionを先にcatchしてしまうと、その後ろではもうNullPointerExceptionをcatchできないのです。これがswtichとは違うところですね。

例外は何かしらの継承関係にあることが多いので、特定のサブクラスを選んでcatchしてその例外だけの処理を行い、それ以外の例外は同じ処理をしたい、という場合は、catchする例外の順序に気を付けなければなりません。

同じことは、ExceptionThrowableでも言えます。ですから、例外の継承関係を知るためにも、APIドキュメントには目を通しておいた方が良いでしょう。

2-3-2.複数の例外を1つのcatchで捕捉する「マルチキャッチ」

なお、Java 7からマルチキャッチ(multi-catch)という構文が追加されました。複数の例外で同じ処理をすればいいなら、以下のように記述ができます。つまり、catchしたい例外へ「または」と指定するわけです。catchブロックをたくさん縦に並べなくてもいいので、プログラムがすっきりします。

ただし、マルチキャッチを使うと、例外の変数の具体的な型(どのクラスとして扱われるか)は、「または」で繋げた例外の最も共通的な例外クラスとなります。言葉だけでは少しわかりづらいので、例を示します。

以下の例で言うと、NullPonterException、IllegalStateException、IllegalArgumentExceptionで共通なのはRuntimeExceptionのサブクラスだということなので、RuntimeException扱いとなります。ですが、FileNotFoundException と EOFException は両方とも IOException のサブクラスですから、IOException扱いです。

この例だと、それぞれの例外には固有のメソッドはないので違いは見えませんが、以下のように固有のメソッドがあった場合は共通となる例外のものしか扱えないということです。

2-4.try-catch文をネストさせた時は

try-catch文はネストできます。つまり、try-catch文の内側にもtry-catch文を書けるのです。

内側のtry-catch文でcatchされなかった例外は、外側のtry-catch文でcatchできます。もしネストしているすべてのtry-catch文で対象の例外がcatchされないのなら、メソッドからthrowされることになります(throws節への記述が必要)

ただし、外側のcatchブロックやfinallyブロックの中で発生した例外は、そのブロック内で処理するか、メソッドからthrowされなくてはなりません。その時点では、もう外側のtry-catchブロックでは捕捉できないのです。

外側と内側のtry-catch文で同じ例外をcatchするようにした場合は、内側のものが優先されます。一つのtry-catch文で処理が完結していれば、外側へは波及しないということです。

なお、メソッドのthrows節を使うということは、メソッドを超えてtry-catch文をネストさせているのだと考えると分かりやすいです。例えば以下だとmethod2→method1の順番に未処理の例外が伝わり、どこかの誰かがcatchするまで続くのです。

2-5.catchした例外をthrowしたい場合

一度catchした例外をthrowして、呼び出し元に処理を任せたい場合があります。その場では例外が発生したというログだけ出力したい場合などです。その場合は、catch内で例外をthrowできます。

当然ながら、throwされた例外へは誰かが対処しなければならないので、さらにこの外側にtry-catch文を書くか、メソッドのthrows節にthrowした例外の型を記述しなければコンパイルエラーになってしまいます。

なお、catchした例外と違う例外をthrowすることもできます。ただし、発生した例外を捨ててしまうと本当に何が発生したかが呼び出し元側で分からなくなってしまうので、新しくthrowする例外には発生した例外の情報を持たせることが普通です。

3.try-catch文を上手に活用するために

3-1.例外をcatchして何もしないのは厳禁!

例えば、以下のようなcatchした例外に対して実質的に何も行っていないソースコードを結構見かけます。仕事で作られたソースコードでも残念ながら見かけます。これは「例外を握り潰す」と言われることがあるパターンで、大変危険なプログラムです。

なぜ危険なのかと言うと、例外が発生していることが誰にも分からないからです。例外が発生した後も、プログラムは何事もなかったかのごとく平然と動き続けてしまうのです。後者も、標準エラー出力をログに出力していればまだいいのですが、捨ててしまっていたなら前者と同じです。

こういうプログラムがまかり通っているプロジェクトでは、エラー対応が後手に回ったり、問題が致命的なレベルになって初めて、別の経路で問題が発覚したりします。そして「何が起きているのかわからない」という状況になるのです。

プログラミングの経験が浅かったり、仕事の納期が近かったりすると、とにかくコンパイルエラーをなくして早く動かしたい一心でこういうことをする人がいます。ですが、急がば回れと言う言葉もあるように、例外へはきちんと対処すべきなのです。そうしないと後から苦労することになります。

3-2.【中級者向け】RuntimeExceptionやErrorのcatchは必要な時だけ!

例外の一種であるRuntimeExceptionやErrorは、必要性がない限りはcatchしないのが普通です。

JavaでのThrowable関連のクラス階層は以下のようになっています。最上位にインターフェイスThrowableがあり、そのサブクラスにExceptionErrorExceptionのサブクラスとしてRuntimeExceptionがあります。実際の例外は、これらのどれかから継承して作られたものです。

Throwable → 全ての例外・エラーのスーパークラス

  ┗Exception → catchされるべき例外のスーパークラス

      ┗RuntimeException → 非チェック例外のスーパークラス

  ┗Error → 通常のプログラムではcatchすべきでない重大なエラー

この中で、メソッドのthrows節にある場合に対処が必要なのはThrowableExceptionの二種類です。逆に言うと、RuntimeExceptionErrorthrows節に明示的に書いてあっても、try-catchの必要はありません。RuntimeExceptionErrorはどんなものが実際にthrowされるかは分からない、ということでもあります。

Javaでの決め事として、RuntimeExceptionやErrorは対処不能なエラーを表現します。つまり、環境不備やプログラムミスなどが原因であり、発生しても明示的なリカバリ処理をするべきではないエラー、という扱いです。きちんと環境構築やテストを行っていれば、実運用上では発生しないはずのものだからです。

ですので、RuntimeExceptionErrorに対して、try-catch文で明示的に補足して処理をすべきケースは少ないと言えるでしょう。ただし、いかなるエラーが発生してもずっと動き続けなければならない種類のプログラムならcatchが必要です(WEBサーバなど、コンピュータに常駐する種類のものです)

一方、Exceptionはプログラムミス等ではなく、通常の処理上で発生しえて、何かしらのリカバリ処理を呼び出し側が行うべきエラーを表現するのに使います。ですから、Exceptionは対処が構文上も必須になっているのです。これにより、発生しうる例外への対応を呼び出し側へ強制できるのです。

ただ、メソッドのJavadocthrows節でRuntimeExceptionErrorが発生すると明示されている場合は、そういうことが起こり得るということは理解しておきましょう。実際に発生した場合の対処がやりやすくなるからですし、そういうエラーが発生しないような予防措置もできるからです。

3-3.【中級者向け】tryブロックの範囲はなるべく狭くしよう!

3-3-1.例外が発生する・しない処理が混在する場合

例外が発生しうる処理がプログラム中に複数あり、例外が発生しない処理と混在している場合は、どちらが良いtry-catch文の書き方でしょうか。理由も含めて、少し考えてみてください。

どうすべきかはプログラマーにより意見は分かれますが、一般的には「tryブロックのスコープはなるべく小さくすべき」と言われます。実際は前者と後者の折衷案的な広さとなることが多く、その上で後者に近いスタイルが良いとされているでしょうか。

tryブロックの範囲が広すぎると、例外が発生しないところも含まれてしまい、どの処理に対する例外処理なのかが不明瞭になります。ただ、一つ一つの処理にいちいち個別のtry-catch文を使っては、ソースコードがとても長くなりますし、見通しも非常に悪くなります。何事も極端すぎてはいけないのですね。

ですので、あいまいな言い方になりますが、現実的には処理のまとまりが良い単位だったり、これらの処理は一塊にして処理結果を保証しなければならないというような単位とすると、見た目上も処理上も、良いバランスのプログラムになるでしょう。

3-3-2.ループの中で例外が発生する場合

また、以下のループ処理内で例外が発生しうる場合への対応は、どちらがより良いtry-catch文の書き方だと思われますか?

この場合はプログラムの仕様によります。例外発生時にfor文自体を中断させるのか、それとも処理を継続すべきなのかの仕様が明確になっていなければ、どちらが良いかの判断はできないからです。

for文を中断させるなら前者かもしれません。プログラム上も、例外が発生した時にはfor文が終了することが明確、かつ構文レベルで確実だからです。後者のスタイルでやるならcatchブロック内でbreakするのですが、意図が少々伝わりづらいでしょうか。それにbreakを書き忘れたらそのまま動いてしまいます。

後者は、例外発生時にはフォローして、処理自体は継続しなければならないケースに適しているでしょうか。前者のスタイルでやるなら、再度for文を続きから実行しなければなりませんが、さすがにそのようにプログラムを作りたいとは思えませんし、意味の分かりづらいプログラムになってしまいます。

なお、for文などの中でtry-catchを繰り返すのは、実行速度に全く影響が出ないわけではありません。でも、それが目に見える形で現れることはほぼないでしょう。ですから、そのプログラム中で例外発生時にはどう動くべきかという仕様をまず明確にして、それに応じて使い分けましょう。

4.まとめ

この記事ではtry-catch分の基本をお伝えしました。tryブロックで例外が発生しうる部分を指定し、catchブロックで発生した例外への処理を行うのが基本パターンです。

例外のcatchの仕方は、過去の構文では1つのcatchブロックに1つの例外しか指定できませんでしたが、今では複数の例外をcatchできるマルチキャッチがありますので、クラスの継承関係に気を付けた上で、積極的に活用しましょう。

プログラムを作る上では、エラー処理は絶対に欠かせません。そして、プログラムは正常系の処理よりもエラー系の処理の分量の方が多いものです。この記事でお伝えしたことを活用して、エラー処理にも気が配られた良いプログラムを作れるようになっていただければ幸いです。