例外処理

これまでプログラミングを行ってきて、様々なエラーに直面したと思います。
本単元ではそのエラーについて学んでいきます。

まず例外とはプログラム実行時に発生する予期せぬエラーのことを言います。
Javaプログラムでは発生した例外の内容により「エラー(Error)」と「例外(Exception)」に区別され、
その違いはプログラムで対処できるかできないかにあります。

「エラー」の場合はプログラムで対処できない致命的な例外を指し、
「例外」の場合はプログラムで対処できる例外を指します。

「com.cmps.error」パッケージを作成、パッケージ内に「Error」クラスを作成しましょう。

エラーパターン3種類

文法エラー

文法エラーはコンパイルエラーが発生した際のエラーで、最も見覚えのあるエラーかもしれません。
例えば以下の様な場合です。

public static void main(String[] args) {
        System.out.println("ERROR")
}

Javaには記載方法が決まっている部分があり、その決まりを守らずにプログラムを書こうとした時に発生するエラーです。
今回の場合、「System.out.println()」に「;」が存在しないことで文法上の間違いがあるとして、
Javaがコンパイルできない、という意味で発生するエラーです。

Eclipseの補助機能として、文法エラーが発生した時にエラーの発生箇所に対して赤下線が表示されるため、
エラーが発生している個所を特定することは簡単です。

ただ、文法エラーの種類によっては、文法が間違っているエラーの箇所を線で指し示すわけではないため、
文法エラーが発生した時には処理の前後も併せて確認することをお勧めします。

論理エラー

public static void main(String[] args) {
        int x = 3;
        int y = 5;
        int a = calc(x,y);
        System.out.println(x + y + "の結果は" + a);
    }
    
    private static int calc(int x, int y) {
        x += x + y;
        return x;
    }

上記の例ではint xとint yの足し算の結果を出力したいのですが、その出力結果は「8の結果は11」です。

x + y + "の結果は" + a

この部分では左側から計算(3+5)されて、その結果である「8の結果は…」と出力されてしまっています。
また、clacメソッドの「 x += x + y;」で「+=」を使用しており、「3+3+5」と同じになるので、11が出力されます。

このように、コンパイルエラーが発生しておらず、実行時にエラーは発生していないものの、
期待される結果と異なる結果を返してしまう事を論理エラーと呼びます。

他の2つと比べて、プログラム上のエラーは発生していないため、原因を特定するのが特に難しいです。
デバッグ機能を使って変数を処理ごとに値の確認を一つ一つ行い、
期待と違うデータが格納されていないかを見ていくことで、ある程度の解決はできそうです。

実行時エラー

public static void main(String[] args) {
        int[] a = new int[1];
        System.out.println(a[1]);
}

赤下線が引かれないので文法上のエラーはありませんが
実行すると「ArrayIndexOutOfBoundsException」が出力されます。
これは配列内に存在しない箱にアクセスした際に発生するエラーです。

実行時エラーとは、このような実行時にJava側で想定していない値を入れたときに発生するエラーで、
エラーの種類によってかなりの数の対処方法があります。

この想定外の事象が「例外」です。

大抵の例外エラーは何らかのエラー内容が一緒に出力されているため、
そこからエラーの推察を行っていくことになります。

try-catch構文

Javaではエラーが発生した場合、そのプログラム実行は中断されてしまうため、処理を継続することができません。
しかし、予め処理でエラーが発生することを想定できていれば事前にエラーが発生した時の処理を指定して、
想定内の処理とみなす事ができます。

このような処理のことを「例外処理(exception handling)」といいます。

例外処理の基本的な構文は以下の通りです。

try {
 処理
 …
} catch ( 例外のクラス 変数名 ) {
 例外が起きた際に行う処理
 …
}

tryブロック内には、例外の発生する可能性のある処理を記述します。
Javaの標準APIにおいては、予め例外処理の発生する可能性のあるメソッドに対しては、
throws ◯◯Exception と定義されています。

catchブロック内では、発生した例外をキャッチします。
複数記述が可能となっています(同じ例外クラスを重複して記述することは出来ない)。

試しに作成してみましょう。
mainメソッド内に以下のソースを記述し、実行しましょう。
なお、これまで記述してきたソースはコメントアウトしておきましょう。

 public static void main(String[] args) {
         try {
             // 配列の定義
             int[] arrayInt = new int[5];
             
             System.out.println("arrayInt[10]に数値を代入");
             arrayInt[10] = 50; // 配列の最大要素数を超えた代入処理
             
             System.out.println("arrayInt[10]に50を代入しました");
         } catch (ArrayIndexOutOfBoundsException e) { // 要素数越えの例外処理
             System.out.println("配列の要素数を超えています");
        }
         System.out.println("処理終了");
    }

実行結果

「arrayInt[10] = 50;」にて配列の要素数を超えて値を代入しているため、
例外「ArrayIndexOutOfBoundsException」が発生します。

次にcatchブロックにて、例外の種類ArrayIndexOutOfBoundsExceptionを指定しています。
これでtryブロック内の処理を中断する代わりに、catchブロック内の処理を行いエラーメッセージを出力しています。

発生した例外とcatchブロック()内の例外の種類が一致し、catchブロックの処理が行われることを
catchブロックで例外を受け取る」や「例外をキャッチ(catch)する」と呼びます。

呼び出したメソッド内での例外と例外処理の有無

例外処理を行わないプログラミングだと、エラーが発生すると、その発生箇所で処理が終了となることは理解できたと思います。
しかし実際には少し違くて、Javaはエラーに対する例外処理がそのメソッド内で見つからない場合には、
呼び出し元のメソッドに戻って対応する例外処理を探す仕組みになっています。
なので、Mainメソッド内で例外が発生しても、それ以上呼び出し元のメソッドに戻ることが出来ず、
例外処理を行わない場合、プログラムが途中で終了していたのです。

例外処理が無い場合

では、Mainメソッドではないメソッドで例外が起きた際に、その例外処理を行わない場合の流れを確認してみましょう。
・Errorクラス

public static void main(String[] args) {
        // calcTestメソッドの処理を呼び出す
        calcTest();
        
        System.out.println("処理終了");
}

static public void calcTest() {
       // 数値を0で除算
       int num = 10 / 0;
        // 結果を表示
       System.out.println("10/0の結果は" + num);
}

実行結果

Exception in thread “main” java.lang.ArithmeticException: / by zero
at com.cmps.error.Main.calcTest(Main.java:41)
at com.cmps.error.Main.main(Main.java:7)

例外の発生箇所として
① calcTest メソッドで例外が発生している箇所(実際の計算部分)と
② mainメソッド内で例外を起こしている処理(メソッド呼び出し部分) の2か所が挙げられています。

処理の流れとしては以下の図の様になります。

例外処理のメソッドを用意してないパターン

①mainメソッドがcalcTestメソッドを呼び出す
②calcTestメソッドにて例外発生
③calcTestメソッド内に例外処理が無いため、結果の表示が実行されない
④mainメソッド内に例外処理が無いため、System.out.printlnが実行されない
⑤mainメソッドに呼び出し元が無いため、途中終了する

例外処理がある場合

次にmainメソッド内に例外処理を追加したプログラムを見てみましょう。
mainメソッド内に以下を記述しましょう。

 public static void main(String[] args) {
        try {
            // calcTestメソッドの処理を呼び出す
            calcTest();
        } catch (ArithmeticException e) { // 0除算の例外処理
            System.out.println("0は除算できません");
        }
        System.out.println("処理終了");
}

今度は「0は除算できません」と「処理終了」が表示されたのではないでしょうか。
calcTestメソッド内には特に例外処理は追加していませんが、mainメソッド内でtry~catch文を記述しています。
これによりcalcTest()メソッド内で例外が発生してもmain()メソッドで例外を処理することができます!

処理の流れは以下の通りです。

例外処理のメソッドを用意しているパターン

①mainメソッドがcalcTestメソッドを呼び出す
②calcTestメソッドにて例外発生
③calcTestメソッド内に例外処理が無いため、結果の表示が実行されない
④mainメソッド内に例外処理があるため、catchブロック内のSystem.out.printlnが実行される
⑤try-catchブロック以降の処理が行われ終了する

除算を行っているのはcalcTest()メソッド内で、
例外を処理しているのはmainメソッドと別になっていますが、途中で終了することなく正しく処理が終了しています。
この結果から例外の流れが呼び出し先から呼び元へ遡っていることが確認できましたね。

今回はmainメソッドに例外処理を記述しましたが、calcTestメソッドに記述しても問題ありません。
例外をどこに組み込む必要があるのかは、作成するプログラムによって大きく変わりますので注意してください。

try-catch-Finally構文

try-catch構文にはその派生としてtry-catch-finallyが存在します。
この処理ではエラーを補足しても、エラーを補足しなくても、必ず最後に処理を行います

例えば例外の発生に関わらず、そのメソッド内で必ず行っておきたい重要な処理がある場合などに使用されます。

基本的な構文は以下の通りです。

try {
 処理
 …
} catch( 例外のクラス 変数名 ) {
 例外が起きたときに行う処理
 …
} finally {
 例外の発生に関わらず、必ず行う処理
}

tryで行った処理は処理を実行中、エラーが発生した箇所が見つかると以降の行は無視されます。
しかし、finallyはtryの処理が中断されて、catchの処理が入った場合やエラーなく実行された場合でも処理が実行される利点があります。

この処理はreturnと特に相性が良いです。

public String getString() {
 String s = "";
 try{
    //sに対する様々な処理1
    //sに対する様々な処理2 ←ここでエラー発生
    //sに対する様々な処理3
    return s
   } catch (Exception e) {
     s = "";
   }
}

この処理でfinallyがなく、もし処理2でエラーが発生してしまうと、returnで値を返せなくなってしまい、
メソッドの呼び出し元で別のエラーが発生してしまう可能性があります。

public String getString() {
 String s = "";
 try{
    //sに対する様々な処理1
    //sに対する様々な処理2 ←ここでエラー発生
    //sに対する様々な処理3
   } catch (Exception e) {
   s = "";
   } finally {
    return s;
   }
}

finallyでreturnを実行することで、エラーが発生してもreturnで値を必ず返却できるようになるため、
エラーが発生するリスクを減らすことができます。これがfinallyの大きな利点です。

try-catch-Finally構文を使った処理を試してみましょう。
mainメソッドに以下のソースを記述しましょう。

    public static void main(String[] args) {
        try {
            // int型配列定義
            int[] arrayInt = new int[10];
            
            // 配列に値を格納
            System.out.println("配列へ値を格納開始");
            arrayInt[10] = 50; // 例外をわざと発生させる
      // arrayInt[0] = 50; //正しく格納する

            
            System.out.println("配列へ値を格納完了");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("配列の要素数を超えています");
        } finally {
            System.out.println("例外処理の最後の処理です");
        }
        System.out.println("処理終了");
    }

実行結果(例外発生時)

今回も最大要素数を超えた代入を行いエラーを発生させています。
では「arrayInt[0] = 50;」をコメントアウト解除し、「arrayInt[10] = 50;」をコメントアウトして実行してみましょう。

実行結果からもわかるように、例外が発生した場合もしなかった場合も、
finallyブロック内で設定したメッセージ出力処理が行われていることが確認できます。

例外処理ではcatchブロックを複数記述することができます。
逆にcatchブロックは省略も可能ですが、tryブロックだけでは例外処理は記述できないので
その際はfinallyブロックを記述する必要があります。

例外の種類

例外と言ってもその種類はたくさんあります。

例外は大きく3種類に分けることが出来ます。
Throwable Error:Error系例外(OutOfMemoryError、ClassFormatError …etc.)
Exception:Exception系例外(IOException、ConnectException …etc.)
RuntimeException:RuntimeException系例外(NullPointException、 ArrayIndexOutOfBoundsException …etc.)

Error系例外はOS根幹にかかわるエラーが多く、コードの軽量化、ループ回避等、PCの処理を意識して対応する必要があるエラーです。
また、エラーの原因がそもそもPCの性能による原因の可能性も高いエラーです。

例外関連のクラスはツリー構造になっており、大元になっている「Throwableクラス」から
「Errorクラス」と「Exceptionクラス」の2つ枝分かれしています(イメージは以下の画像)。

「Exceptionクラスに属するクラス」は、クラス名の最後に必ず「Exception」が付きます。
「Errorクラスに属するクラス」と「Exceptionクラスに属するクラス」の区別は、このネーミングルールによって判断することができます。

「Exceptionクラスに属するクラス」は「RuntimeExceptionクラスに属するクラス(Exception系例外)」と
「それ以外のExceptionクラスに属するクラス(RuntimeException系例外)」にさらに分類されます。

「RuntimeExceptionクラスに属するクラス(Exception系例外)」は、
例外処理を記述しなくてもコンパイルできる例外のことで、非チェック例外ともいいます。

「それ以外のExceptionクラスに属するクラス(RuntimeException系例外)」は、
例外処理を記述しないとコンパイルエラーになる例外のことで、 チェック例外ともいいます。
チェック例外は、例外が送出される可能性があるクラスのため、必ず例外処理を記述しなければなりません

代表的なエラーを以下にまとめました。

エラー内容分類説明
OutOfMemoryErrorThrowableプログラムが使用可能なメモリを使い切り、もう新しいオブジェクトを作成できなくなった場合に発生します。
StackOverflowErrorThrowableメソッド呼び出しの再帰が無限に続いてスタックが溢れた場合に発生します。
ArithmeticExceptionRuntimeExeption0で割り算をしようとした場合や、
不正な算術演算を行おうとした場合に発生します。
ArrayIndexOutOfBoundsExceptionRuntimeExeption配列の有効な範囲外のインデックスにアクセスしようとした場合に発生します。
ClassCastExceptionRuntimeExeptionキャストが失敗し、異なる型のオブジェクトをキャストしようとした場合に発生します。
DateTimeParseExceptionRuntimeExeption文字列を日時に変換しようとした際に、解釈できない形式の文字列が渡された場合に発生します。
ConcurrentModificationExceptionRuntimeExeptionコレクションが変更されている間に
、反復処理が行われた場合に発生します。
IllegalArgumentExceptionRuntimeExeptionメソッドに渡された引数が予期しない値や、範囲外の値である場合に発生します。
IllegalStateExceptionRuntimeExeptionオブジェクトが許可されていない状態で、メソッドが呼び出された場合に発生します。
InputMismatchExceptionRuntimeExeptionスキャナや入力ストリームで期待された形式と、異なる入力が提供された場合に発生します。
InterruptedExceptionRuntimeExeptionスレッドが割り込まれ、スレッドが割り込みに、応答しなかった場合に発生します。
NullPointerExceptionRuntimeExeptionヌル(null)の値を持つオブジェクトに対して、メソッドやフィールドにアクセスしようとした場合に発生します。
NumberFormatExceptionRuntimeExeption文字列を数値に変換しようとした際に、数値として解釈できない文字列が渡された場合に発生します。
StringIndexOutOfBoundsExceptionRuntimeExeption文字列の有効な範囲外のインデックスに、アクセスしようとした場合に発生します。
SQLExceptionException(SQL)データベースに関連する一般的なエラーが発生した場合に投げられる例外。
SQLIntegrityConstraintViolationExceptionException(SQL)データの整合性制約(主キーや外部キーなど)が破られた場合に発生する例外。
SQLSyntaxErrorExceptionException(SQL)SQL文が構文エラーを含む場合に発生する例外。
SQLTimeoutExceptionException(SQL)クエリの実行がタイムアウトした場合に発生する例外。
SQLFeatureNotSupportedExceptionException(SQL)JDBCドライバがサポートしていない機能が使用された場合に発生する例外。
PSQLArrayExceptionException(SQL)PostgreSQLの配列関連のエラーが発生した場合に投げられる例外。
PSQLDataExceptionException(SQL)データの変換や操作に関連するエラーが発生した場合に投げられる例外。
PSQLQueryExceptionException(SQL)PostgreSQLのクエリ実行に関連する一般的なエラーが発生した場合に投げられる例外。
FileNotFoundExceptionException存在しないファイルにアクセスしようとした場合に発生します。
InstantiationExceptionExceptionクラスが抽象クラスまたはインターフェースであり、
インスタンスを作成できない場合に発生します。
IOExceptionException入出力操作中にエラーが発生した場合に発生します。
NoSuchElementExceptionExceptionコレクションから要素を取得しようとした際に、
要素が存在しない場合に発生します。
SecurityExceptionExceptionセキュリティに関連する操作が
許可されていない場合に発生します。
UnsupportedOperationExceptionException特定の操作がサポートされていない場合に発生します。

throwとthrows

Exceptionはtry-catchで捕まえることができるほかに、throwを使って意図的にExceptionを発生させることも可能です。
また、発生させたエラーはthrowsを使ってその時点では処理を一旦無視するという方法もあります。

開発では様々なクラスを作成して一つの大きなシステムを作りますが、各クラス毎でcatchするのではなく、
1か所でまとめてcatchするクラスを作ってエラーの特定を楽にしようとするときや、処理を中断する場所を調整する時に使います。

具体的にどのような時に使うのか見ていきましょう。
「com.cmps.error」パッケージ内に「Sub」クラスを作成しましょう。
・Errorクラス

    public static void main(String[] args) {
        try {
            Sub.subA();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        try {
            Sub.subB();
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            subC();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
   public static void subC() throws Exception {
       System.out.println("同じクラスでthrowsしても動きは同じです");
       throw new Exception("subCが実行されています");
   }

・Subクラス

public class Sub {
    public static void subA() throws Exception {
        System.out.println("Exceptionを発生させます");
        throw new Exception();
    }
    public static void subB() throws Exception {
        System.out.println("メッセージ付きでExceptionを発生させます");
        throw new Exception("subBが実行されます");
    }
}

実行結果

Exceptionを意図的に発生させるためには「throw new Exception」で発生が可能です。
throwsは、発生させたExceptionをメソッド呼び出し元に送る、という意味があります。

例えば、subAでは「throw new Exception」を実行すると、
Errorクラスのmainメソッド側でcatchをして例外のエラー内容を出力しています。
他メソッドで発生させたExceptionをメソッド呼び出し元になるsubA()メソッドでcatchをして捕まえているのです。

subB,subCについても同様にメソッドでthrowされたものをメインメソッド側で受け取っています。
なお、Exceptionをthrowする時に()内に文字列を入れることでException発生時にメッセージを指定する事も可能です。

練習問題

「com.cmps.error」パッケージ内に「ErrorQuestion」クラスを作成しましょう。

問1:以下のプログラムから発生すると思われる例外は以下のどれでしょう。また正しく動作するように修正しましょう。

public static void main(String[] args) {
        int[] numbers = {10,20,30};
        System.out.println(numbers[3]);
}

選択肢
1.OutOfMemoryError
2.NullPointerException
3.ArrayIndexOutOfBoundsException
4.InputMismatchException

問2:以下のプログラムから発生すると思われる例外は以下のどれでしょう。また正しく動作するように修正しましょう。

public static void main(String[] args) {
        String sampleString = "text";
        int num = Integer.parseInt(sampleString);
        System.out.println(num);
}

選択肢
1.IOException
2.NumberFormatException
3.IllegalArgumentException
4.OutOfMemoryError

問3.以下のソースにおいて、divisionメソッドは割り算の結果を返します。
ただし、0で割った場合(ArithmeticException)と数値以外が入力された場合(InputMismatchException)に例外が発生します。

このプログラムを 「try-catch」 を使って安全に実行できるようにしましょう。

public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("割られる数を入力してください。");
        int child = scanner.nextInt();
        
        System.out.println("割る数を入力してください。");
        int parent = scanner.nextInt();
        
        int ans =division(child, parent);
        System.out.println("結果:"+ ans);
}

// 割り算用メソッド
public static int division(int child, int parent) {
        return child / parent;
}

Scannerクラス
標準入力(キーボードからの入力)を取得、プログラム内で処理する際に利用します。

以下の手順で実装可能です。
1.Scannerクラスのインスタンスを作成し、コンストラクタの引数にSystem.inを指定
2.next()やnextLine()、nextInt()等のメソッドを用いて、インスタンスから入力内容を取得

詳しい使い方については以下のサイトなどを参考にしてください。
【Java入門】標準入力を取得、出力する方法

タイトルとURLをコピーしました