Lesson 7

例外処理

Lesson 7 Chapter 1
エラーと例外

前レッスンでは、Javaで標準的に提供されている様々なパッケージ、クラス、インターフェースを見てきました。既にあるものを応用することで、より効率よくコードが書けることが分かったと思います。

本レッスンで扱うのは「例外処理」です。例外処理とは、プログラムの実行中に予期せぬエラーが発生した場合に、そのエラーに対処する仕組みのことです

まずは、例外処理に関わる概念を学んでいきましょう。

Javaでは、エラーは「コンパイルエラー」と「実行時エラー」に分けることができます。

コンパイルエラーとは

コンパイルとは、プログラムをコンピューターが理解できる形式に変換することを指します。このコンパイルを行う際に発生するエラーをコンパイルエラーといいます。コンパイラがソースコードの解析や変換を行う際に、文法エラーや型の不一致などのエラーが発生した場合に発生します。たとえば、変数名に不適切な文字が含まれたり、セミコロンが抜けていたりする場合です。コンパイルエラーが発生すると、コンパイラはプログラムのコンパイルを中止し、エラーメッセージを出力します。

コンパイルエラーは、プログラムを実行する前に必ず解消する必要があります。コンパイルエラーが発生している場合、Javaコンパイラは、プログラムを実行可能なバイナリファイルに変換しません。また、コンパイルエラーは実行中に発生するエラーではないため、実行中に発生するエラーに対処する例外処理の対象にはならないことが分かります。

実行時エラーとは

実行時エラー(ランタイムエラー)とは、プログラムが実行中に発生するエラーで、文法的なエラーではなく、プログラムの動作に関する問題が原因です。例えば、存在しない変数を参照しようとした場合にはNullPointerExceptionが発生し、範囲外の配列インデックスを参照しようとした場合にはArrayIndexOutOfBoundsExceptionが発生します。実行時エラーは、プログラムをコンパイルする段階では検出できず、プログラムを実行した後に発生するため、例外処理が必要となります。つまり、実行時エラーは例外処理で対処できるエラーです。

例外とは

例外処理が実行時エラーを防ぐのは分かりました。しかし、例外と実行時エラーは少し異なる概念です。例外とは、プログラムの実行中に発生する異常な状態を表すオブジェクトです。例外が発生することを「例外がスローされる」といいます。例外をスローされたままにすると、実行時エラーが発生します。しかし、プログラム内でスローされた例外をキャッチすることで、実行時エラーを防ぐことができます。まとめると、例外処理とは、実行中にスローされた例外をキャッチすることで、実行時エラーを防ぐことです。

ここまでコンパイルエラーと実行時エラーの違い、そして例外について学びました。

Java-7_1

コンパイルエラーとは、コンパイルする段階で発生するエラーであり、実行時エラーとは、プログラムの実行時に発生するエラーでした。そして、例外処理が対象とするのは実行時エラーであり、プログラム実行中にスローされた例外をキャッチすることで実行時エラーの発生を防ぐことが分かりました。では次に「例外」についてより詳しく見ていきましょう。

検査例外と非検査例外

Javaにおいて、例外は検査例外と非検査例外の二つに分類されます。以下は、Error、Exceptionクラスの継承関係を示した図です。

Java-7_2

検査例外(Checked Exception)はJava実行環境以外の環境が原因の例外です。検査例外は、必ず例外処理をしないといけません。継承関係で説明すると、検査例外にあたるのは、Exceptionのサブクラスの中でRuntimeExceptionクラス以外のものが該当します。

主な検査例外として以下のような例外があげられます。

  • IOException: 入出力に関する問題があった場合に発生

  • FileNotFoundException: ファイル入出力において、目的のファイルがなかった場合に発生

  • ParseException: 解析中に予想外のエラーがあった場合に発生

  • SQLException: データベース・アクセス時のエラーがあった場合に発生

非検査例外(Unchecked Exception)は、実行中のプログラムが原因で発生する例外で、例外処理は任意になっています。継承関係で説明すると、Errorクラス及びそのサブクラス、そしてRuntimeExceptionクラスおよびそのサブクラスが該当します。

Errorクラスのサブクラスにあたる例外は以下のようなものがあります。

  • AssertionError: アサーションが失敗すると発生

  • StackOverflowError: アプリケーションでの再帰の回数が多すぎる場合に発生

  • NoClassDefFoundError: クラス定義が見つからない場合に発生

RuntimeExceptionクラスのサブクラスにあたる例外は以下のようなものがあります。

  • ArrayIndexOutOfBoundsException: 不正なインデックス(添字)を使って配列がアクセスされると発生

  • ClassCastException: 継承関係にないオブジェクト同士をキャストしようとすると発生

  • IllegalStateException: メソッドの呼び出しが、不正、不適切な時に行われると発生

  • DateTimeException: 日付/時間の計算時に問題があると発生

  • ArithmeticException: 算術計算で例外が発生した場合に発生(整数をゼロで除算するなど)

  • NullPointerException: オブジェクトが必要な際に、nullを使うと発生

ここまで、例外処理に関わる概念を見てきました。次のチャプターからは、実際に例外処理のやり方について、見ていきましょう。

Lesson 7 Chapter 2
基本的な例外処理

前チャプターでは、例外とは何かについて見てきました。本チャプターでは、例外処理に関わる基本的な構文を見ていきます。具体的には、try-catch文、try-catch-finally文、throw句、throws句を見ていきます。

try-catch文、try-catch-finally文

try-catch文は、例外処理を行うために使用されます。tryブロック内で例外が発生した場合、catchブロックがスローされた例外をキャッチし、処理が実行されます。try-catch文の構文は次のようになります。

Main.java
try {
    // 例外が発生する可能性のある処理
} catch (例外クラス名 変数名(慣習的に e を使う)) {
    // 例外が発生した場合に実行する処理
}                 

例えば、ファイルを読み込む処理で例外が発生した場合、以下のようにtry-catch文を使って例外処理を行うことができます。

Main.java
try {
  BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
  String line = reader.readLine();
  System.out.println(line);
} catch (FileNotFoundException e) {
  System.err.println("ファイルが見つかりません。");
} catch (IOException e) {
  System.err.println("入出力エラーが発生しました。");
}

上記の例では、FileNotFoundExceptionが発生した場合には「ファイルが見つかりません。」というエラーメッセージを表示し、IOExceptionが発生した場合には「入出力エラーが発生しました。」というエラーメッセージを表示します。

また、複数の例外を処理する場合は、複数のcatchブロックを使用することができます。しかし、catchブロックで指定する例外に継承関係がある場合は、サブクラス側から記述します。以下のコードを見てみましょう。

Main.java
try {
    // 例外が発生する可能性のある処理
} catch (NullPointerException e) {
    // 例外が発生した場合に実行する処理
} catch (RuntimeException e) {
    // 例外が発生した場合に実行する処理
}                        

二つのcatchブロックでそれぞれ、NullPointerExceptionRuntimeExceptionをキャッチしようとしています。NullPointerExceptionRuntimeExceptionは、継承関係にありますが、サブクラスであるNullPointerExceptionを先に記述しているので、正しい順序です。

次にtry-catch-finally文を見ていきましょう。finallyブロックは、try-catch文の最後に書くことができます。 finallyブロック内の処理は、tryブロック内で例外が発生した場合でも、catchブロック内で例外が処理された場合でも必ず実行されます。構文は以下のようになります。

Main.java
try {
    // 例外が発生する可能性のある処理
} catch (例外クラス名 e) {
    // 例外が発生した場合に実行する処理
} finally {
    // 必ず実行する処理
}

例えば、ファイルを開いて処理を行い、最後にファイルを閉じる処理で例外が発生した場合、以下のようにtry-catch-finally文を使って例外処理を行うことができます。

Main.java
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("sample.txt"));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    System.out.println("ファイルを読み込めませんでした:" + e.getMessage());
} finally {
    if (br != null) {
        try {
            br.close();
        } catch (IOException e) {
            System.out.println("ファイルを閉じることができませんでした:" + e.getMessage());
        }
    }
}

この例では、sample.txtというファイルを読み込んで、その中身を標準出力に出力する処理を行っています。tryブロックでは、ファイルの読み込みに失敗した場合に発生するIOExceptionをキャッチして、エラーメッセージを表示します。finallyブロックでは、ファイルを閉じる処理を行っています。tryブロックでファイルを開いた場合、必ずfinallyブロックでcloseメソッドを呼び出して、ファイルを閉じるようにしています。ファイルを開いた後は必ず閉じる必要があるため、finallyブロックで処理を行うことで、例外が発生してもファイルを閉じることができます。

例外処理は、プログラムの実行中に発生した不正な状態やエラーを処理するために重要です。try-catch文を使用することで、プログラムの実行を続行することができます。また、catchブロック内では、例外が発生した原因を特定し、それに対応する処理を行うことができます。

try-catch文は、Javaの例外処理の中心的な構文です。try-catch文を使用しない場合、プログラムは例外が発生した時点で強制終了します。そのため、例外処理を行わないと、プログラムが予期せず終了し、データの整合性やセキュリティー上の問題を引き起こす可能性があります。

例外処理を行うことで、プログラムの実行継続ができ、エラーを適切に処理することができるため、非常に重要な概念となります。

try-catchの入れ子

try-catch文は、入れ子にすることもできます。これは、try-catchブロックの中に別のtry-catchブロックを記述することで実現されます。入れ子になったtry-catch文は、内側のtry-catchブロックが例外を処理できなかった場合に、外側のtry-catchブロックがその例外を処理するという形で、例外処理を行うことができます。

例えば、次のようにtry-catch文を入れ子にすることができます。

Main.java
try {
    // 例外が発生する可能性のある処理
    try {
        // 例外が発生する可能性のある処理
    } catch (ExceptionType1 e) {
        // 例外が発生した場合に実行する処理
    }
} catch (ExceptionType2 e) {
    // 例外が発生した場合に実行する処理
}              
                    

このように、try-catch文を入れ子にすることで、複雑な例外処理を行うことができます。

一方で、try-catch文を入れ子にする場合には、他の方法を検討することも重要です。例えば、throwsキーワードを使用することで、例外処理を行うことなく、例外を上位のメソッドに伝播させることもできます。(throwsについては後述します)

例外のthrow

Javaでは、例外を発生させるためにthrowキーワードを使用することができます。throwキーワードを使用することで、特定の例外クラスのインスタンスを生成し、それを発生させることができます。

次のようにthrowを使用して、例外を発生させることができます。

Main.java
throw new 例外クラス名;

例外を発生させることで、プログラムの実行状態が異常になったことを知らせることができます。例外が発生した場合には、例外処理を行うことで、プログラムを再開することができます。

throwキーワードは、メソッド内で使えます。しかし、throwキーワードを使う場合は、そのメソッド内で例外をキャッチして処理するか、またはthrowsキーワードを使って、例外を呼び出し元に投げる必要があります。では、throwsキーワードを見ていきましょう。

throwsによる例外の指定

throwsキーワードを使うと、メソッド内で例外が発生した場合に、その例外を呼び出し元にスローすることができます。つまり、例外処理を呼び出し元に任せることができます。

Main.java
public void method1() throws Exception{
    if(condition) {
        throw new Exception("Invalid condition");
    }
}
                    

上記のように、method1メソッド内で、特定の条件下において例外を発生させることができます。[method1()]メソッドにはthrowsキーワードがついており、指定されている例外は、メソッド内で発生しうるExceptionクラスで同一です。これにより、[method1()]メソッド内で例外が発生した場合、この例外は、メソッドの呼び出し元にスローされます。

Main.java
try {
   method1(); //呼び出し元
} catch (Exception e) {
    // 例外が発生した場合に実行する処理
}              

上記のコードでは、[method1()]メソッドを呼び出しています。呼び出し元でこのように例外処理を書いていれば、メソッド側から例外がスローされても処理が出来ます。

Lesson 7 Chapter 3
例外の継承

前チャプターでは、例外処理の具体的な方法を見てきました。本チャプターでは、例外の継承関係を見ていきます。

Throwableクラス、Errorクラス、Exceptionクラス

Throwableクラスは、すべての例外とエラーのスーパークラスです。このクラスには、例外やエラーをスローするためのメソッドや、スタックトレースを取得するためのメソッドなどが定義されています。そして、Throwableクラスの直接のサブクラスが、ErrorクラスとExceptionクラスです。それぞれ見ていきましょう。

Errorクラスは、Javaで発生するシステムエラーを表します。これらのエラーは、一般的にアプリケーションの問題ではなく、システムの設定や資源の問題に起因します。例えば、メモリ不足や、クラスファイルが見つからないなどがあります。これらのエラーは、通常アプリケーションが処理を続行することはできず、アプリケーションを終了する必要があります。

Exceptionクラスは、実行時例外(RuntimeException)を含め、Javaで定義されるほとんどの例外の親クラスとして定義されており、例外が発生した際に実行される処理を記述することができます。具体的には、例外の原因を解決するためのメッセージを表示する、ログに書き込む、例外を無視するなどの処理を行うことができます。

独自例外クラスの定義

Exceptionクラスを継承することで、Javaでは独自の例外クラスを定義することができます。独自の例外クラスを定義することで、アプリケーション固有の例外を扱うことができます。

MyException.java
class MyException extends Exception {}

独自例外は以上のコードで定義できますが、一般的には、エラーメッセージを指定するためのコンストラクタを定義します。

MyException.java
class MyException extends Exception {
      public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

1つ目のコンストラクタは、親クラスのコンストラクタを呼び出しています。この場合、親クラスで定義されている引数のないコンストラクタを呼び出しています。2つ目のコンストラクタは、メッセージを引数として受け取り、親クラスのコンストラクタにメッセージを渡しています。

例外をスローする場合、自分で定義した「MyException」クラスを使用することができます。これにより、より詳細な例外情報を提供することができます。

例えば、以下のようなコードで「MyException」クラスを使用することができます。

MyException.java
public class Test {
  public static void main(String[] args) throws MyException {
    int age = 17;
    if (age 

この例では、年齢が18歳未満の場合、自分で定義した「MyException」クラスを使用して例外をスローしています。スローされた例外は、呼び出し元でキャッチされ、適切に処理することができます。

独自の例外クラスを定義する際には、既存の例外クラスを継承することもできます。RuntimeExceptionクラスを継承することで、非検査例外を定義することもできます。独自の例外クラスを定義することで、アプリケーション固有の例外を扱うことができます。また、例外の詳細な情報を提供することができ、アプリケーションの動作をより正確に制御することができます。

このように、例外処理はプログラムの信頼性を高めるために非常に重要な概念です。しっかりと例外処理を行うことで、アプリケーションの安定性やエラー発生時のメッセージ表示など、ユーザーにとっても使いやすいアプリケーションを開発することができます。次のレッスンでは、データベースについて見ていきます。データベースとアプリケーションの接続には、多くの例外処理を必要とします。本レッスンの知識を活かして取り組んでいきましょう。