Java
2022.01.21
Javaのtry-catch文で例外処理!finallyやthrowを使いこなそう
2023.11.18

プログラムを作る場合に気をつけたいのが「バグ」です。


きちんと動くプログラムを作ることが理想ではありますが、バグのまったくないプログラムを作るのは不可能と言われています。


Javaでは、正しくバグと付き合うための方法として「try-catch」という概念を採用しています。


1.Javaのtry-catch文とは?

Java_try_catch01

Javaでは、プログラム中で発生したエラーを「例外(Exception)」として定義します。
try-catch文は、例外が発生した際に、そのエラーを掴む(キャッチする)ために利用します。


try-catch文を利用することで、プログラムの実行中に発生したエラーの内容に応じて、特別な処理を指定できるのです。

①try-catch文が必要な理由

プログラムの実行中にエラーが発生した場合、本来であればそのプログラムは異常終了してしまいます。


個人向けのプログラムであれば再起動するだけで良い場合もありますが、業務アプリはそうはいきません。異常終了により業務が継続できないということは、多大な損害を生み出すことも考えられます。


そこで、本来であれば終了してしまうプログラムの「エラー」を制御するために、try-catch文を利用するのです。


try-catch文をうまく活用してエラーをキャッチすることで、プログラムが異常終了することを防止できます。

また、プログラムは終了しないため、エラーが発生した箇所だけスキップしたり、途中からやり直したりすることも可能です。



2.try-catch文の基本的な使い方


ここでは、try-catch文の基本的な使い方を紹介します。

①try-catchの基本構文

try-catch文は、その名前の通りtryとcatchのふたつのブロックで構成します。


例外が発生する可能性のある処理をtryブロックの中に記述し、例外が発生した時に実行する特別な処理をcatchブロックの中に記述します。


さらに、catch句に例外クラスを指定することで、その例外に応じた処理を記述できるのです。

try {
    
    // 例外が発生する可能性のある処理
    
} catch(Exception e) { 
    
    // エラー(Exceptionクラスの例外)が発生した場合の処理
}

②finally文に必ず実行する処理を記述

tryブロックの途中でエラーが発生した場合、エラーが発生したあとの処理は実行されません。


どこでエラーが発生したとしても最後に実行したい処理がある場合には、「finally」ブロックに記述します。


finallyブロックの中に記述した処理は、例外の発生有無に関係なく、最後に実行されます。

try {
    
    // 例外が発生する可能性のある処理

} catch(Exception e) {

    // エラーが発生した場合の処理

} finally {

    // 必ず最後に実行する処理

}

③try-catch-finally文の処理の流れ

try-catch-finally文を記載した際、例外が発生した場合・していない場合で実行される処理が異なります。


まずは、次のようなプログラムを考えます。

/** 数値計算を行うクラス */
public class NumberConverter {
    
    /**
     * 入力の文字列を数値に変換し、10で割った数を返す
     * 
     * @param value 入力文字列
     * @return 計算結果
     */
    public static Integer calcNumber(String value) {

        try {
            //文字列のvalueをを数値に変換
            Integer number = Integer.parseInt(value); // --- (1)
    
            System.out.println("入力文字: " + number);
    
            // 10を入力文字で割った数を返却
            return 10 / number; // ------------------------- (2)
    
        } catch (NumberFormatException e) {
            System.out.println("数字が入力されていません。"); // - (3)
        } finally {
            System.out.println("最終処理です。"); // ---------- (4)
        }
    
        return null; // ------------------------------------ (5)
    }
}


例外が発生した場合

例外が発生した場合、例外発生箇所よりもあとのtryブロック内の処理は実行されず、catchブロック内の処理が実行されます。


例として、次のような呼び出しをします。

Integer result = NumberConverter.calcNumber("a");
System.out.println("計算値: "+result);

この実行結果が以下のとおりです。

数字が入力されていません。
最終処理です。
計算値: null

引数に数値以外の文字列が指定された場合、(1)で「NumberFormatException」をスローします。

すると、(2)の処理は実行されず、(3)、(4)の処理が実行され、最後に(5)が実行されます。

例外が発生しない場合

例外が発生しない場合には、tryブロックの中の処理がすべて実行されます。

Integer result = NumberConverter.calcNumber("5");
System.out.println("計算値: "+result);

これを実行すると、以下のように実行されます。

入力文字: 5
最終処理です。
計算値: 2

このように、エラーがない場合には、10を引数で割った数である「2」が返却されます。

return文がある場合

クラスメソッドの場合、tryブロックの中にもreturnを書くことが可能です。


この場合、(2)でreturnされている時点で、(5)の処理は実行されませんが、(4)のfinallyブロックの処理は実行されます。


このように、tryブロックの中にreturnがあった場合でもfinallyの処理は実行されます。



3.try-catch文をより効率的に使う方法


try-catch文の基本的な使い方を理解したところで、効率的にtry-catch文を利用する方法を紹介します。

①複数の例外をキャッチし、それぞれ異なる処理を行う

tryブロック内の処理に対して、複数の例外を定義できます。


先ほどのコードでは、引数に”0”を指定すると、0除算となってしまいエラーが発生してしまいます。


そのため、0が入力されたときのエラーをキャッチできるようにコードを追加してみましょう。

public static Integer calcNumber(String value) {
    try {
        // 文字列のvalueを数値に変換
        Integer number = Integer.parseInt(value);

        System.out.println("入力文字: " + number);

        // 10を入力文字で割った数を返却
        return 10 / number;

    } catch (NumberFormatException e) {
        System.out.println("数字が入力されていません。");
    } catch(ArithmeticException e) {
        System.out.println("0は入力しないでください。");
    } finally {
        System.out.println("最終処理です。");
    }

    return null;
}

このように、発生した例外に応じて異なる処理を記述することで、発生した例外に応じて適切なメッセージを表示することが可能です。

例外の継承関係に注意する

複数の例外をキャッチする場合、発生した例外の型を上から順番に評価し、対応する型と一致する場合にはそのcatch句を実行します。
このとき、例外の継承関係に注意しましょう。


たとえば、IOExceptionはExceptionを継承しているため、IOExceptionのインスタンスはExceptionとして扱うこともできます。


以下のようにIOExceptionよりも先にExceptionのcatchを書いてしまうと、IOExceptionが発生したとしても、Exceptionの中のcatchブロックの処理が実行されてしまいます。


そのため、以下のようなコードを書いてしまうとコンパイルエラーが発生します。

try {
    
    // 例外が発生する可能性のある処理
    
} catch (Exception e) {
    System.out.println("Exceptionが発生しました。");
} catch (IOException e) {
    System.out.println("IOExceptionが発生しました。");
}

IOExceptionとExceptionで処理を分けたい場合には、継承元となるスーパークラスの定義をうしろに書くようにしましょう。

try {
    
    // 例外が発生する可能性のある処理
    
} catch (IOException e) {
    System.out.println("IOExceptionが発生しました。");
} catch (Exception e) {
    System.out.println("Exceptionが発生しました。");
} 

こうすることで、IOExceptionが発生した場合には「IOExceptionが発生しました。」というメッセージが表示されます。

②複数の例外をまとめてキャッチし、同じ処理を行う

複数の例外に対して同じ処理を行いたい場合には、catch句に複数の例外を定義することが可能です。

try {
    
    // 例外が発生する可能性のある処理

} catch(IOException | NullPointerException e) {
    System.out.println("IOExceptionかNullPointerExceptionが発生しました。");
} catch(Exception e) {
    System.out.println("何かしらのエラーが発生しました。");
}

この場合、IOExceptionかNullPointerExceptionが発生すると、対応するcatch句の中のコードが実行されます。


エラーの内容によって処理を分岐させない場合には、このテクニックを活用することで効率の良いプログラムを作ることが可能です。

③try-catch文を入れ子にする

try-catch文は入れ子にすることができます。つまり、tryやcatch、finallyブロックの中にtry-catch文を宣言することも可能です。

try {
    // 処理省略
} catch(Exception e) {
    
    try {
        // 最終処理
    } catch(Exception ex) {
        // さらにエラーが発生した場合の処理
    }
}

④throwで意図的に例外を発生させる

プログラム上、意図的にエラーとして処理させたい場合もあります。


その場合には、tryブロックの任意の場所でthrowを利用することで、意図的に例外を発生させることも可能です。

public static Integer calcNumber(String value) {
    try {
        // StringをNumberに変更
        Integer number = Integer.parseInt(value);

        System.out.println("入力文字: " + number);

        // 先に0かを判定する
        if (number == 0) {
            throw new NumberFormatException("0は入力不可能です。");
        }

        // 10を入力文字で割った数を返却
        return 10 / number;

    } catch (NumberFormatException e) {
        System.out.println("入力値が不正です。");
    } finally {
        System.out.println("最終処理です。");
    }

    return null;
}

ArithmeticExceptionは、0除算の場合に発生するエラーなので、先にif文で判定した上でNumberFormatExceptionを発生させています。


こうすることで、発生する例外を限定できるのです。

⑤throwsを用いて呼び出し元で例外を処理する

メソッドの中でエラーが発生した場合、メソッド内でキャッチすることも可能ですが、呼び出し元にキャッチの処理を強制する方法もあります。


メソッドの宣言に「throws 例外クラス」を追加することで、メソッドが例外をスローする可能性があることを宣言します。

public static Integer calcNumber(String value) throws NumberFormatException, ArithmeticException{

    // StringをNumberに変更
    Integer number = Integer.parseInt(value);

    System.out.println("入力文字: " + number);

    // 10を入力文字で割った数を返却
    return 10 / number;
}

throwsが指定されたメソッドを呼び出す場合には、以下のように例外に応じたtry-catch文を定義しましょう。

try {
    // メソッド呼び出し
    Integer result = calcNumber("2");
} catch (NumberFormatException | ArithmeticException e) {
    System.out.println("入力値が不正です。");
}




4.Exceptionクラスの種類

Java_try_catch02

Javaでは、発生するエラーに応じた例外クラスが存在します。つまり、例外クラスの内容を見るだけで、どのようなエラーが発生しているのかが分かるようになっているのです。


ここでは、よく目にする例外クラスを紹介します。

例外説明
NullPointerException値がnullのオブジェクトへアクセスすると発生
IOExceptionファイルや通信などのIOに関する問題があった場合に発生
FileNotFoundExceptionファイルが見つからない場合に発生
IllegalArgumentException不正な引数の場合に発生
IndexOutOfBoundsException配列やリストのサイズを超えてアクセスした場合に発生


例外の名前と説明を確認することで、ある程度起きていることを予測することが可能です。デバッグの際に例外が発生しても焦らずに、例外内容をしっかりと確認しましょう。



5.オリジナルの例外クラスを定義する方法


オリジナルの例外クラスを作成することも可能です。


「Exception」を継承して作ることで、新しい例外を定義できます。

public class MyLocalException extends Exception{

    public MyLocalException() {
        super("独自の例外です");
    }
}

定義した例外は、他の例外と同じように利用できます。

public static int multiple(int val) throws MyLocalException{

    if (val < 0) {
        throw new MyLocalException();
    }

    return val * val;
}




6.try-catch文を使う際の注意点


try-catch文はプログラミングをする上で便利に使える反面、注意して使う必要があります。
ここでは、try-catch文を使う上で注意すべき点を紹介します。

①例外をキャッチして何もしないのはNG

例外が発生した場合には、何かしらの問題が起きていることが多く、バグが原因であることも少なくはありません。


バグが原因で正常に動作していない場合には、例外の内容をもとに調査します。


例外には「スタックトレース」と呼ばれる情報が格納されており、以下のような情報が取得できます。

・どのソースコードで例外が発生したのか

・メソッドの呼び出された順番


例外のスタックトレースは、バグの発生箇所や原因を特定するために重要な意味を持ちます。例外をキャッチした場合には、エラー内容を出力するなどの処理を行うことが通例です。


もし例外をキャッチしても何もしない場合、何が起きるのでしょうか。


例として、次のようなメソッドを考えましょう。

public static Integer calcNumber(String value) {
    try {
        // StringをNumberに変更
        Integer number = Integer.parseInt(value);

        System.out.println("入力文字: " + number);

        // 10を入力文字で割った数を返却
        return 10 / number;

    } catch (Exception e) {
    } 

    return null;
}

途中で例外が発生した場合でも、このメソッドは何事もなかったかのようにnullを返却して処理を終了します。


例外をキャッチした時にエラー内容を出力していればバグの調査に役立ちますが、何もしない場合にはここで例外が発生したことすら検出できなくなるのです。


この問題は、大規模開発であるほど影響が大きくなるといえます。キャッチの中身を記載し忘れるだけで例外を発見できなくなり、結果としてバグを見つけるために多大な労力をかけてしまうこともあります。


そのため、例外をキャッチした場合には何かしらの処理をしっかりと記載するように心がけると良いでしょう。

②tryブロックの範囲はなるべく狭くする

try-catch文は便利に使える反面、きちんと整理して使わないと予期しないバグを生み出すこともあります。


tryブロックの中で例外が発生した場合、以降の処理は実行されず、対応する例外のcatchブロックが実行されます。


例として、次のようなコードを考えてみましょう。

void methodA() throws FileNotFoundException {
    // 処理は省略
}

void methodB() {
    // 処理は省略
}

void methodC() throw NumberFormatException, IOException{
    // 処理は省略
}

この3つのメソッドを呼び出すコードを以下のように書いてみます。

try {
    methodA();
    
    methodB();
    
    methodC();
    
} catch (FileNotFoundException e) {
    // methodAのエラー
} catch (NumberFormatException e) {
    // methodCのエラー
} catch (IOException e) {
    // methodCのエラー
}

キャッチすべき例外が多く、処理の流れを追いにくくなってしまっています。


以下のように適切な分割を行うとで、最終的なコード量は増えますが、そのぶん可読性の高いコードになります。

try {
    methodA();
} catch (FileNotFoundException e) {
    // methodAのエラー
} 

methodB();

try {
    methodC();
} catch (NumberFormatException | IOException e) {
    // methodCのエラー
} 

tryブロックの中で例外が発生した場合、以降の処理が実施されません。

これにより予期しないバグが発生することもありますので、例外のブロックは適切に行うようにしましょう。


この記事をシェア