Java入門|例外処理が不要な例外とは

全部の例外に身構えなくていいとわかると、Javaの例外処理はぐっと整理しやすくなる

ここまでで、Javaでは例外が送出される可能性のあるメソッドを使うとき、
その場で try-catch で受け取って処理するか、
あるいは throws を付けて呼び出し元へまかせるか、
どちらかを選ぶ必要があることを見てきました。

ただし、ここでひとつ大事な例外があります。
実は、すべての例外について必ず try-catch または throws が必要というわけではありません。

Javaの例外には、利用者が明示的に対処を求められるものと、そうではないものがあります。
この違いを知らないまま学ぶと、
「例外は全部 catch しないといけないのかな」
「throws は毎回必要なのかな」
と混乱しやすくなります。

ドラゴンボールの世界でたとえるなら、すべての異常事態に同じ手順で対応するわけではありません。
たとえば、宇宙船そのものが崩壊しかけているような致命的な事態と、戦士がうっかり修行名簿の欄外に名前を書いてしまったようなミスとでは、扱い方が違いますよね。
前者は現場の工夫でどうこうする話ではなく、後者はそもそもコードの書き方や使い方の見直しで防ぐべきことが多いです。

Javaでも同じで、Error の仲間RuntimeException の仲間は、原則として try-catch や throws を必ず書かなくてもよいことになっています。
ここでは、その理由と意味を、ドラゴンボールの世界観に置き換えながら、やさしく整理していきます。

すべての例外で同じ対応が必要なわけではない

これまで見てきた独自例外のように、Exception を継承した例外の中には、
メソッドを使う側が

  • try-catch で処理する
  • throws で呼び出し元へまかせる

のどちらかを選ぶ必要があるものがあります。

ですが、Javaでは次の2つのグループに属するものについては、同じルールを必ずしも求めていません。

グループ基本の考え方
Error のサブクラス致命的な問題なので通常は例外処理しない
RuntimeException のサブクラス必ずしも明示的な例外処理を要求しない

つまり、これらは
try-catch を書かなくてもよい
throws を書かなくてもよい
という扱いになることがあります。

ここが、例外処理の大きな分かれ道です。

なぜ例外処理が不要な場合があるのか

理由は、例外の性質が違うからです。

Error の場合

Error は、プログラムの実行そのものが厳しいような重大な問題を表します。
たとえば、メモリ不足のように、通常のアプリケーション側で気軽に立て直せる話ではないことがあります。

こうした問題に対して、毎回 try-catch を書いて細かく対処することは、ふつうは考えません。
そのため、Error の仲間は明示的な例外処理の対象としては扱われにくいです。

ドラゴンボール風にたとえるなら、
修行メニューの入力ミスではなく、界王星そのものが崩れ始めているような事態です。
現場で帳簿を直すとか、別ルートに処理を回すといった話ではなく、そもそも継続が難しいレベルの問題です。

RuntimeException の場合

RuntimeException は、実行中に起こる例外のうち、プログラムの書き方や使い方が原因になっていることが多いグループです。
たとえば、配列の範囲外アクセスや、null を使ってしまうミスなどがこの系統に含まれます。

これらは、もちろん起きてほしくない問題です。
ただし、Javaの考え方としては、
毎回必ず try-catch で囲わせるより、まずは正しいコードを書くことで防ぐべきもの
とされることが多いです。

ドラゴンボール風にいえば、
修行名簿が5人分しかないのに10番目へ記録しようとするのは、戦士の運用ミスに近いです。
こういうものは、警報が鳴るたびに正式な申請書を出すより、最初から正しい範囲で記録するようにコードを直すほうが本質的です。

例外の分類をもう一度整理しよう

ここで、例外のクラス構造を「例外処理が必須かどうか」という観点から整理すると、かなり分かりやすくなります。

クラス位置づけ基本的な扱い
Error致命的なエラー通常は try-catch や throws を求めない
Exception一般的な例外例外処理が必要なものを含む
RuntimeExceptionException の一部明示的な例外処理を必須としない
Exception のうち RuntimeException 以外いわゆる checked exceptiontry-catch か throws が必要

この最後の
Exception のうち RuntimeException 以外
が、学習の流れでよく出てくる「明示的な処理が必要な例外」です。

一方で、RuntimeException はその仲間ではあっても、扱いが少し違います。

throws が付いているメソッドがなぜ目印になるのか

クラスを使う側は、そのメソッドに throws が書かれているかどうかを見ることで、
そのメソッドが利用者に明示的な対応を求めているかどうかを知ることができます。

たとえば、あるメソッド宣言が次のようになっていたとします。

public void setTraining(int l, double g) throws TrainingException

この場合、TrainingException が送出される可能性があるので、利用者は

  • try-catch で受け取る
  • あるいは、自分のメソッドにも throws を付ける

のどちらかを考えなければなりません。

一方、RuntimeException 系の例外については、こうした形で必ず宣言しなくてもよいことになっています。
そのため、利用者に対して「必ずここで処理してください」とまでは要求しない作りになります。

クラス設計者と利用者の役割分担

例外処理のしくみは、単にエラーを止めるためだけのものではありません。
クラスを作る人と、それを使う人の役割分担を柔軟にするためのしくみでもあります。

ドラゴンボールの世界で考えると、修行装置を設計するブルマのような立場の人が、すべての使い方、すべての現場判断まで決め切ってしまうとは限りません。
実際に使うのは悟空やベジータかもしれませんし、状況によって現場の対応は変わります。

Javaでも同じで、クラスの設計者がすべてのエラー処理を内部で決め打ちしてしまうと、利用者は柔軟に使えなくなることがあります。
そこで、

  • この例外は利用者に判断をまかせる
  • この例外はそこまで厳密に強制しない
  • このエラーは通常は現場処理の対象ではない

というように、例外の種類ごとに役割を分けているわけです。

checked exception と unchecked exception の感覚

用語として整理すると、次のように考えると理解しやすいです。

分類代表的な考え方
checked exception明示的に処理するか、throws で渡す必要がある
unchecked exception明示的な try-catch や throws を必須としない

このうち unchecked exception にあたるのが、主に RuntimeException とそのサブクラスです。
Error も、利用者に通常の例外処理を要求しない側に入ります。

つまり、今回のテーマである「例外処理が不要な例外」とは、主に
Error の仲間と RuntimeException の仲間
のことだと考えると整理しやすいです。

サンプルプログラムで考え方を整理しよう

ここでは、ドラゴンボール風の独自例外を使ったクラス設計の流れを確認しながら、
「処理を求める例外」と「必ずしも求めない例外」の違いを意識できるようにしていきます。

ファイル名:Sample5.java

class TrainingException extends Exception
{
}

// 修行装置クラス
class TrainingMachine
{
    private int level;
    private double gravity;

    public TrainingMachine()
    {
        level = 0;
        gravity = 0.0;
        System.out.println("修行装置を起動しました。");
    }

    public void setTraining(int l, double g) throws TrainingException
    {
        // 重力が負の値なら独自例外を送出する
        if(g < 0)
        {
            TrainingException e = new TrainingException();
            throw e;
        }
        else
        {
            level = l;
            gravity = g;
            System.out.println("修行レベルを" + level + "に、重力を" + gravity + "に設定しました。");
        }
    }

    public void show()
    {
        System.out.println("現在の修行レベルは" + level + "です。");
        System.out.println("現在の重力設定は" + gravity + "です。");
    }
}

class Sample5
{
    public static void main(String[] args)
    {
        TrainingMachine machine1 = new TrainingMachine();

        try
        {
            machine1.setTraining(3, -50.0);
        }
        catch(TrainingException e)
        {
            System.out.println(e + " が送出されました。");
        }

        machine1.show();
    }
}

このサンプルで押さえたいこと

このプログラムで送出している TrainingException は、Exception を継承した独自例外です。
しかも RuntimeException ではありません。
そのため、setTraining メソッドを使う側は、例外処理を考える必要があります。

実際に main メソッドでは、try-catch を使って受け取っています。

try
{
    machine1.setTraining(3, -50.0);
}
catch(TrainingException e)
{
    System.out.println(e + " が送出されました。");
}

これは、
throws が付いているメソッドを呼び出すなら、その利用者が処理を考える必要がある
というルールをよく表しています。

ここで大事なのは、これが「すべての例外で必ずそうなる」わけではないことです。
TrainingException のような例外だから、利用者に明示的な対応が求められているのです。

RuntimeException の仲間ならどう考えるのか

ここでは具体的な別プログラムは増やしませんが、考え方だけはしっかり押さえておきたいところです。

たとえば、配列の範囲外アクセスで起きる ArrayIndexOutOfBoundsException は、RuntimeException のサブクラスです。
この場合、毎回

  • try-catch を書く
  • あるいは throws を付ける

ことを、Java は必須にはしていません。

もちろん、必要に応じて catch してもかまいません。
ただし言語としては、
「まずは配列の使い方を見直しましょう」
という立場を取っています。

つまり RuntimeException は、
処理を書いてもよいが、書かなくてもコンパイル上は問題にしないことが多い例外
と考えると分かりやすいです。

Error の仲間はどう見るべきか

Error の仲間は、さらに性質が異なります。
これらは、通常のアプリケーションコードで細かく受け止めて立て直すというより、
深刻で致命的な問題
として扱われることが多いです。

そのため、学習の基本としては
「通常の例外処理の主役は Exception 側」
「Error は原則として通常の try-catch の中心にはしない」
と押さえておくと整理しやすいです。

ドラゴンボール風にいえば、
修行記録の記入ミスではなく、神殿そのものが崩壊し始めているような状況です。
そうなると、現場での細かな受け止め方より、そもそも継続できるかどうかのほうが問題になります。

図で全体の考え方を整理する

この図は、設計者が throws で利用者へ例外処理をゆだねる流れと、Error や RuntimeException の仲間は明示的な処理を必須としないという分類の違いを整理したものです。
特に、throws が付いているメソッドを使うときには利用者が対応方法を選ぶ必要がある一方で、すべての例外が同じ扱いではないことが分かります。

この図から分かることは、主に次の通りです。

分かること内容
throws の意味利用者に例外処理の判断を委ねる合図になる
利用者の選択肢try-catch するか、さらに throws で渡すかを選べる
Error の位置づけ通常の例外処理の中心にはしない
RuntimeException の位置づけ明示的な処理を言語として強制しない

図で checked exception と unchecked exception を整理する

この図は、例外クラスの種類によって、明示的な例外処理が必要かどうかが異なることを示しています。
特に、Exception の中でも RuntimeException 以外のものと RuntimeException の仲間とで扱いが違います。

クラス設計者が全部処理しないほうがよい理由

例外処理は、親切に見えても、設計者が全部を抱え込めばよいというものではありません。
なぜなら、クラスの利用場面はさまざまで、どのように対処したいかは利用者ごとに違うからです。

たとえば、ある利用者は

  • エラーメッセージを表示して再入力させたい

かもしれませんし、別の利用者は

  • ログだけ残して処理を続けたい

かもしれません。
さらに別の利用者は

  • もっと上位の管理クラスに判断をまかせたい

かもしれません。

このように、対応方法は一つではありません。
だからこそ Java には、throws で利用者に判断をまかせる仕組みがあり、同時に RuntimeException のように「必ずしも明示的な処理を強制しない」例外もあるのです。

ドラゴンボールの世界でも、ブルマが装置を作るときに、すべての現場対応を1通りに固定してしまったら、悟空たちが状況に応じて柔軟に動けなくなってしまいます。
設計者が警報の仕組みを用意し、利用者が現場で判断できるようにするからこそ、全体として使いやすくなります。

ここで押さえておきたいポイント

最後に、このテーマで大事な点を整理しておきます。

ポイント内容
すべての例外で同じ対応は不要例外クラスの種類によって扱いが違う
Error の仲間致命的な問題なので通常は明示的な例外処理をしない
RuntimeException の仲間try-catch や throws を必須としない
Exception のうち RuntimeException 以外try-catch または throws が必要になることが多い
throws の意味利用者に例外処理の判断をゆだねる合図
設計の意義利用者ごとに柔軟なエラー対応ができる

例外処理を学び始めたばかりのときは、どうしても
「例外が出たら全部 catch」
のように考えがちです。
でも Java は、そこまで単純には作られていません。

実際には、例外の種類によって

  • 必ず利用者に対応してほしいもの
  • 状況に応じて現場判断にゆだねるもの
  • そもそも通常の例外処理の対象とは言いにくいもの

が分かれています。

この違いが見えてくると、throws の意味も、RuntimeException の立ち位置も、ぐっと自然に理解できるようになります。
ドラゴンボールの世界でも、ちょっとした修行ミスと宇宙規模の崩壊では、同じ指示で対処しませんよね。
Java の例外もそれと同じで、問題の種類ごとに扱い方が変わるように設計されているのです。