Java道|例外処理が必須ではない例外

全部の例外に身構えなくても大丈夫。
Javaの例外は、種類によって「必ず対応が必要なもの」と「明示的な対応が必須ではないもの」に分かれています。

Javaの例外処理を学んでいると、最初は次のように感じやすいです。

「例外が起きるなら、全部 try-catch で囲まないといけないのかな」
「例外があるメソッドには、毎回 throws を書かないといけないのかな」

たしかに、これまで学んできた TrainingException のような独自例外では、メソッドを使う側が try-catch で受け取るか、throws でさらに呼び出し元へまかせる必要がありました。

しかし、Javaの例外はすべて同じ扱いではありません。

例外には、明示的な例外処理が求められるものと、必ずしも求められないものがあります。

今回の記事では、鬼殺隊の重力修行装置を例にしながら、例外処理が必須ではない例外 について整理していきます。使用するプログラムは Sample5.java です。

鬼滅の刃風にたとえると、すべての異常事態に同じ手順で対応するわけではありません。

異常の種類鬼滅の刃風のイメージJavaでの扱い
修行装置の設定ミス重力設定がマイナスになっている独自例外で利用者に対応を求める
名簿の記入ミス5人分の欄しかないのに10番目へ記録するRuntimeException 系のミス
本部全体の重大異常訓練場そのものが崩れ始めるError 系の重大問題

このように、同じ「異常」でも性質が違います。

Javaでは、この違いを例外クラスの分類によって表しています。

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

Javaでは、例外が起きる可能性がある場合でも、すべてに対して必ず try-catch や throws を書く必要があるわけではありません。

大きく分けると、次のように整理できます。

分類基本の考え方
Error のサブクラス致命的な問題なので、通常は明示的な例外処理を求めない
RuntimeException のサブクラスプログラムの使い方や書き方で防ぐべきことが多く、明示的な例外処理を必須としない
Exception のうち RuntimeException 以外明示的な try-catch または throws が必要になることが多い

ここで大切なのは、Exception の仲間だから全部同じ ではないという点です。

Exception の中にも、RuntimeException の仲間と、それ以外の例外があります。

例外の種類明示的な処理
RuntimeException 系必須ではない
RuntimeException 以外の Exception 系必要になることが多い

たとえば、今回の TrainingException は Exception を継承していますが、RuntimeException は継承していません。

そのため、TrainingException を送出するメソッドを呼び出す側は、try-catch で受け取るか、throws で呼び出し元へ渡す必要があります。

Error の仲間は通常の例外処理の中心ではない

Error は、Javaの例外階層の中では Throwable の下にあるグループです。

しかし、通常のアプリケーションで細かく try-catch して立て直す対象としては扱われにくいです。

分類内容
Error実行環境やシステム全体に関わる深刻な問題を表すことが多い
メモリ不足など、通常の処理で簡単に回復しにくい問題
基本的な考え方通常は細かく catch して処理する対象にしない

鬼滅の刃風にたとえると、Error は修行者の記録ミスではなく、訓練場そのものが崩れ始めるような事態です。

たとえば、修行名簿の番号を間違えたなら、記録係が修正できます。
しかし、訓練場全体の結界が壊れ、建物が崩れそうになっている場合は、通常の修行処理を続けるどころではありません。

鬼滅の刃風の状況Javaのイメージ
修行名簿の記入ミスプログラム側で見直せる可能性がある
重力設定の不正値例外として利用者に判断を求められる
訓練場全体の崩壊Error に近い重大な問題

そのため、Error の仲間は、学習の基本では「通常の例外処理の主役ではない」と考えると分かりやすいです。

RuntimeException の仲間は明示的な処理を必須としない

RuntimeException は、実行中に起こる例外のうち、プログラムの書き方や使い方が原因になることが多いグループです。

代表的なものには、次のような例外があります。

例外起きる状況
ArrayIndexOutOfBoundsException配列の範囲外にアクセスした
NullPointerExceptionnull の参照先を使おうとした
NumberFormatException数字に変換できない文字列を変換しようとした

これらは、もちろん起きてよい問題ではありません。

しかし、Javaはこれらに対して、毎回必ず try-catch または throws を書くことまでは求めません。

なぜなら、RuntimeException 系の多くは、例外が起きたあとに受け止めるよりも、そもそも起きないようにコードを直すことが大切だからです。

鬼滅の刃風にたとえると、5人分の修行名簿しかないのに、10番目の欄へ記録しようとするようなものです。

この場合、警告を受け取る仕組みを毎回書くよりも、まずは次のように考えるほうが自然です。

問題本質的な対策
存在しない欄に記録しようとした添字の範囲を確認する
null の隊士情報を使おうとしたオブジェクトが作られているか確認する
数字でない札を数値として読んだ入力値を確認する

RuntimeException は、必要なら catch できます。
ただし、言語として明示的な例外処理を必須にはしていません。

図:例外処理が必須かどうかの全体像

↓クリックすると拡大表示されます。

この図が示していること

この図では、Javaの例外階層を「例外処理が必須かどうか」という観点で整理しています。

Throwable の下には Error と Exception があります。
Error は通常の例外処理の中心にはしません。

Exception の中でも、RuntimeException の仲間は明示的な try-catch や throws を必須とはしません。

一方、RuntimeException 以外の Exception は、明示的な処理が必要になることが多いです。

図の要素分かること
Error致命的な問題として扱われやすい
RuntimeException明示的な例外処理を必須としない
RuntimeException 以外の Exceptiontry-catch または throws が必要になることが多い

この図から分かることは、例外はすべて同じ扱いではなく、種類によって対応の必要性が変わるということです。

checked exception と unchecked exception

ここで、よく使われる用語も整理しておきます。

Javaの例外は、明示的な処理が必要かどうかという観点で、次のように分けられます。

分類内容
checked exceptiontry-catch で処理するか、throws で渡す必要がある例外
unchecked exception明示的な try-catch や throws を必須としない例外

unchecked exception にあたる代表が、RuntimeException とそのサブクラスです。

Error も、通常は明示的な例外処理を要求されない側として考えます。

種類分類のイメージ
RuntimeException 系unchecked exception
Error 系unchecked 側として扱われる
RuntimeException 以外の Exception 系checked exception

今回の Sample5.java で使う TrainingException は、Exception を継承しています。

class TrainingException extends Exception
{
}

ただし、RuntimeException は継承していません。

そのため、TrainingException は checked exception として扱われます。

つまり、setTraining メソッドが TrainingException を送出する可能性を持つなら、呼び出し側はその対応を考える必要があります。

Sample5.javaで使うTrainingExceptionの位置づけ

ここでは、前の記事で使用した Sample5.java を使用します。このプログラムでは、TrainingException という独自例外を作っています。

これは、鬼殺隊の重力修行装置で不正な重力設定が指定されたときに送出される例外です。

ファイル名: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 を継承しています。

class TrainingException extends Exception
{
}

この TrainingException は、RuntimeException の仲間ではありません。

そのため、setTraining メソッドでは、次のように throws TrainingException を書いています。

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

これは、呼び出し側に対して、

「このメソッドでは TrainingException が送出される可能性があります」

と知らせるための宣言です。

TrainingExceptionはなぜ明示的な処理が必要なのか

TrainingException は、Exception を継承した独自例外です。

RuntimeException ではないため、Javaは呼び出し側に対して、明示的な対応を求めます。

つまり、setTraining メソッドを呼び出す側は、次のどちらかを選ぶ必要があります。

対応内容
try-catch で受け取るその場で TrainingException に対応する
throws でさらに渡す自分では処理せず、呼び出し元へまかせる

今回の Sample5.java では、main メソッドの中で try-catch を使って受け取っています。

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

これは、TrainingException が送出された場合に、main メソッド内で受け止める形です。

鬼滅の刃風にたとえると、重力修行装置が危険な設定値を検知して警告札を投げたとき、近くに待機している支援隊士がその札を受け取るようなものです。

Javaの処理鬼滅の刃風のイメージ
setTraining が TrainingException を送出する修行装置が警告札を投げる
throws TrainingException修行場の入口に注意書きを出す
catch(TrainingException e)支援隊士が警告札を受け取る
machine1.show()修行装置の現在状態を確認する

実行結果

このプログラムを実行すると、次のようになります。

鬼殺隊の重力修行装置を起動しました。
TrainingException が送出されました。
現在の修行レベルは0です。
現在の重力設定は0.0です。

setTraining(3, -50.0) では、重力 g に -50.0 を渡しています。

machine1.setTraining(3, -50.0);

setTraining メソッドでは、g < 0 の場合に TrainingException を送出します。

if(g < 0)
{
    TrainingException e = new TrainingException();
    throw e;
}

そのため、level と gravity に新しい値は設定されません。

項目
渡した修行レベル3
渡した重力設定-50.0
実際の level0
実際の gravity0.0
理由throw により通常の設定処理が中断されたため

このように、TrainingException は明示的に扱う必要がある例外として設計されています。

RuntimeExceptionならどう違うのか

ここで、RuntimeException の仲間と比べてみましょう。

たとえば、ArrayIndexOutOfBoundsException は RuntimeException のサブクラスです。

配列の範囲外アクセスで起きる例外です。

int[] power = new int[5];
power[10] = 9000;

このようなコードでは、実行時に ArrayIndexOutOfBoundsException が発生します。

しかし、Javaはこのコードに対して、

「必ず try-catch で囲んでください」
「必ず throws を書いてください」

とは求めません。

例外系統明示的な処理
TrainingExceptionException 系で RuntimeException ではない必要
ArrayIndexOutOfBoundsExceptionRuntimeException 系必須ではない

RuntimeException 系の例外は、主にプログラムの書き方や使い方によって防ぐべきものとして扱われます。

鬼滅の刃風にたとえると、配列の範囲外アクセスは、修行名簿の存在しない欄に記録しようとするようなものです。

このようなミスは、毎回支援隊士を待機させるよりも、まず名簿の範囲を正しく確認するほうが本質的です。

RuntimeException 系の例本質的な対策
配列の範囲外アクセス添字が範囲内か確認する
null の利用オブジェクトが存在するか確認する
数値変換失敗変換前に入力内容を確認する

もちろん、必要に応じて RuntimeException 系の例外を catch することはできます。
ただし、Javaの言語ルールとして明示的な処理は必須ではありません。

図:TrainingExceptionとRuntimeExceptionの違い

↓クリックすると拡大表示されます。

この図が示していること

この図では、TrainingException と RuntimeException 系の例外の違いを比較しています。

TrainingException は Exception を継承していますが、RuntimeException ではありません。
そのため、明示的な try-catch または throws が必要になります。

一方、ArrayIndexOutOfBoundsException のような RuntimeException 系の例外は、明示的な処理を必須とはしません。

比較TrainingExceptionRuntimeException 系
分類checked exceptionunchecked exception
明示的な処理必要必須ではない
考え方利用者に対応を求めるまずコードの見直しで防ぐ
鬼滅の刃風修行装置が正式に出す警告札名簿の使い方ミス

この図から分かることは、例外の種類によって「利用者に対応を強制するかどうか」が変わるということです。

throwsが付いているメソッドは利用者への合図

throws は、メソッドを使う側にとって大切な合図です。

Sample5.java では、setTraining メソッドに throws TrainingException が付いています。

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

これは、メソッドを使う側に対して、次のように伝えています。

「このメソッドでは、TrainingException が送出される可能性があります。対応方法を考えてください」

そのため、main メソッドでは try-catch を使っています。

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

このように、throws は設計者から利用者への注意書きになります。

立場役割
クラス設計者危険な状態を TrainingException として定義する
setTraining メソッドthrows で例外の可能性を宣言する
クラス利用者try-catch または throws で対応を選ぶ

鬼滅の刃風にたとえると、修行装置の設計者が、入口に次のような札を出している状態です。

「この装置は、危険な重力値が入ると警告を出します。使用者は対応を準備してください」

この注意書きがあることで、使う側は警告が出る可能性を前提に処理を組み立てられます。

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

例外処理は、単にエラーを止めるためだけの仕組みではありません。

クラスを作る人と、クラスを使う人の役割を分けるための仕組みでもあります。

TrainingMachine クラスを作る人は、重力がマイナスなら危険だと判断できます。

if(g < 0)
{
    TrainingException e = new TrainingException();
    throw e;
}

しかし、その例外が起きたときにどう対応するかは、利用者によって変わるかもしれません。

利用者がしたい対応
メッセージを表示したい重力設定が不正です と表示する
再入力させたい正しい重力値をもう一度入力させる
ログに残したい異常値を記録して処理を続ける
上位の管理処理へ任せたい自分のメソッドにも throws を付ける

もし TrainingMachine クラスの中で、すべての対応を固定してしまうと、利用者は柔軟に動けません。

そのため、TrainingMachine は「危険な値を検知して例外を送出する」役割を持ち、利用者は「その例外をどう扱うか」を決めます。

鬼滅の刃風にたとえると、修行装置は危険値を検知して警告札を出します。
その警告札を見て、現場の支援隊士が修行を止めるのか、再設定させるのか、本部へ報告するのかを決めるわけです。

例外処理が必須ではない例外がある理由

ここまで見ると、例外処理が必須ではない例外がある理由も見えてきます。

Javaは、例外の性質によって役割を分けています。

例外の種類Javaの考え方
checked exception利用者に明示的な対応を求める
RuntimeException 系コードの使い方を見直して防ぐことが多い
Error 系通常のアプリケーション処理で立て直しにくい

たとえば、TrainingException は、クラス設計者が「この異常は利用者に対応を考えてほしい」と示すために使っています。

一方、RuntimeException 系の例外は、主にプログラムの誤った使い方によって起きることが多いため、毎回明示的な処理を書くよりも、まず正しいコードに直すことが重視されます。

Error 系は、さらに深刻な問題を表すため、通常の try-catch の中心にはしません。

鬼滅の刃風に整理すると、次のようになります。

Javaの分類鬼滅の刃風の例え対応の考え方
checked exception修行装置が正式に出す警告札利用者が受け取り方を考える
RuntimeException修行名簿の使い方ミスコードや使い方を見直す
Error訓練場全体の崩壊通常の現場処理では扱いにくい

このように、Javaはすべての例外を同じルールで扱うのではなく、性質ごとに分けています。

図:設計者と利用者の役割分担

↓クリックすると拡大表示されます。

この図が示していること

この図では、例外処理におけるクラス設計者と利用者の役割分担を表しています。

TrainingMachine を作る側は、危険な重力設定を検知し、TrainingException を送出するように設計します。

setTraining メソッドには throws TrainingException が付いているため、利用者は例外が起きる可能性を知ることができます。

利用者は、その例外を try-catch で受け取るか、さらに throws で上位へ渡すかを選びます。

役割内容
クラス設計者どんな状態を例外にするか決める
throws利用者に例外の可能性を知らせる
クラス利用者try-catch または throws で対応を選ぶ
RuntimeException明示的な対応を必須としない
Error通常の例外処理の中心にしない

この図から分かることは、例外処理は単なるエラー対応ではなく、クラス設計者と利用者の役割を分けるための仕組みでもあるということです。

明示的な処理が必要な例外と不要な例外を見分ける感覚

学習段階では、次のように考えると整理しやすいです。

見分け方考え方
throws が付いている呼び出し側に明示的な対応を求めている可能性が高い
RuntimeException 系try-catch や throws は必須ではない
Error 系通常の例外処理の主役ではない
Exception だが RuntimeException ではないchecked exception として扱われることが多い

Sample5.java の setTraining メソッドは、throws TrainingException を持っています。

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

そのため、呼び出し側は次のように備えています。

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

この流れは、checked exception の考え方を理解するうえでとても分かりやすい例です。

一方で、RuntimeException 系の例外では、Javaは必ずしもこのような明示的処理を求めません。

例外処理が必須ではないから放置してよいわけではない

ここで注意したいのは、RuntimeException 系の例外は「処理が必須ではない」だけであって、「気にしなくてよい」という意味ではないことです。

たとえば、ArrayIndexOutOfBoundsException が起きるコードを放置してよいわけではありません。

int[] power = new int[5];
power[10] = 9000;

このコードは、コンパイル上は try-catch や throws がなくても問題にされないことがあります。

しかし、実行すれば配列の範囲外アクセスで例外が発生します。

つまり、明示的な例外処理が必須ではないからといって、安全なコードになるわけではありません。

誤解正しい理解
RuntimeException は処理不要だから無視してよい明示的な処理が必須ではないだけで、原因の修正は必要
Error は catch しなくてよいから気にしなくてよい通常の例外処理で扱いにくい重大問題として理解する
checked exception だけ気にすればよいunchecked exception もバグの原因として重要

鬼滅の刃風にたとえると、名簿の範囲外に記録しようとするミスは、正式な警告札の受け取り手順が必須ではないとしても、直さなくてよいわけではありません。

むしろ、最初から正しい欄に記録するように修正することが大切です。

この記事で押さえておきたいこと

例外処理が必須ではない例外を理解するには、例外の分類を見ることが大切です。

ポイント内容
すべての例外で同じ対応は不要例外の種類によって扱いが違う
Error の仲間致命的な問題として、通常の例外処理の中心にはしない
RuntimeException の仲間明示的な try-catch や throws を必須としない
Exception のうち RuntimeException 以外try-catch または throws が必要になることが多い
TrainingExceptionException を継承し、RuntimeException ではないため明示的な対応が必要
throws の意味利用者に例外処理の判断を求める合図
unchecked exception明示的な処理は必須ではないが、原因の見直しは重要

Javaの例外処理では、例外があるから全部 catch する、という考え方ではなく、例外の種類に応じて対応を考えることが大切です。

TrainingException のように、クラス設計者が利用者に明示的な対応を求める例外もあります。

一方で、RuntimeException のように、明示的な処理を必須とはせず、コードの正しさで防ぐべきものもあります。

さらに、Error のように、通常の例外処理の中心にはしない重大な問題もあります。

鬼滅の刃風に言えば、修行装置が正式に出す警告札、修行名簿の使い方ミス、訓練場全体の崩壊は、それぞれ扱い方が違います。

この違いが分かると、try-catch、throws、RuntimeException、Error の関係がかなり整理しやすくなります。