Javaプログラミング入門 16章 [例外]

Java

こんにちは。ECF Techブログ
担当 Michiharu.Tです。

Javaプログラミング入門記事の第16章をお送りいたします。
第16章のテーマは「例外」です。

本連載の初回および章立ての一覧については下記のリンクから確認できます。

Javaプログラミング入門 0章
0章 Javaプログラミングを始めよう こんにちは。ECF Techブログ 担当 Michiharu.Tです。 この記事はプログラミング言語のJavaを、実際に動かしながら学びたい人のための学習教材となっています。手を多く動かすこと、感覚を...

本章では、Javaプログラムの実行時エラーとその対処法についてご紹介します。Javaではプログラムの実行時に発生するエラーのことを例外と呼びます。

16-1 例外

16-1-1 例外の発生

例外が発生するプログラムの例として、次のような計算用クラスを考えます。

プログラム例(Calc.java)

public class Calc{
    public static void warizan(int x, int y){
        int ans = x / y;
        System.out.println(x + "÷" + y + "=" + ans + "です");
    }
}

割り算をしてくれるwarizanというクラスメソッドをもったクラスです。次にメインクラスを作成します。上のCalcクラスを利用するプログラムとなっています。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        Calc.warizan(9,3);
    }
}

Calc.javaおよびMain.javaをコンパイルし、Mainクラスを実行すると、次の実行結果となります。

実行結果

9÷3=3です

warizanメソッドを使って9÷3の計算を行ない、結果を表示しています。

では次に、同じwarizanメソッドを次のようなプログラムで動作させてみましょう。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        Calc.warizan(9,0);
    }
}

実行結果

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Calc.warizan(Calc.java:3)
        at Main.main(Main.java:3)

この実行結果は概ね下図のような内容を表しており、実行時エラー(例外) が発生していることを表しています。

例外の発生

mainメソッドにおいて引数として90を渡しているため、warizanメソッド内で9÷0の計算が発生しています。ですが、0で割り算を行うことはできないため 例外が発生します。

16-1-2 例外クラス

Javaでは例外をクラスとして表現します。16-1で例外を発生させた際、実行結果中にjava.lang.ArithmeticExceptionという記述が出てきましたが、これは例外を表すクラスの1つです。Javaでは例外の種類ごとにクラスが定義されており、代表的なものには次のようなものがあります。

例外クラス名 概要
NullPointerException NULL値が設定された参照型変数を利用したときに発生
ArrayIndexOutOfBoundsException 指定した配列の添字が範囲外の値だった場合に発生
FileNotFoundException 指定したファイルが見つからない場合に発生

16-1-3 検査例外と非検査例外

また、Javaの例外クラスは検査例外非検査例外にわかれます。2つの違いは例外処理が必須かそうでないかの違いです。検査例外に分類されるクラスは、適切な例外処理を行っていないとコンパイルエラーとなってしまいます。16-1-1のサンプルプログラムで発生したArithmeticExceptionは非検査例外に該当するため、例外処理を行っていなくてもコンパイルエラーは発生しません。

次節では例外の処理方法についてにご紹介します。

Javaの各例外クラスは、下図のように様々なインターフェースやクラスを継承しています。
例外処理クラス群※図は上に示されるものほど、親クラスであることを表します。

各例外をどのように処理すべきかは、その例外クラスの親クラスがどのクラスかによって下表のように決まります。

親クラス 例外処理
Exception 必須
RuntimeException 任意
Error 不可

16-2 例外処理

16-2-1 try-catch文

Javaには例外に対処する方法がいくつかあります。その方法の1つがtry-catch文を使った方法です。try-catch文の書き方を示します。

try{
    //例外が発生しうる処理を記述
} catch(例外クラス名 変数名){
    //例外が発生した際の処理を記述
}

catchブロックの()内は引数のような形になっています。例外クラス名の部分には発生しうる例外のクラス名を記述します。次は、この構文を使ってCalc.javaを改良した例を示します。

プログラム例(Calc.java)

public class Calc{
    public static void warizan(int x, int y){
        try{
            int ans = x / y;//(1)
            System.out.println(x + "÷" + y + "=" + ans + "です");
        } catch(ArithmeticException e) {
            System.out.println("割り算に失敗しました。");//(2)
        }
    }
}

Main.javaでは再度下のプログラムを使用し、例外を発生させます。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        Calc.warizan(9,0);
    }
}

実行結果

割り算に失敗しました。

解説
try-catch文を使うと、正常時と例外発生時で処理の流れを次のように変えることができます。

try-catch

  • 正常時はtryブロック内のみの処理を行います。
  • 例外発生時はtryブロック内の処理を中断して、catchブロックに処理が移ります。

今回のプログラム例ではMainクラスではwarizanメソッドに引数9,0が渡されて呼び出されています。したがって、Calcクラスのコメント(1)部分で0での割り算が行われてしまい、ArithmeticExceptionの例外が発生します。例外が発生するとcatchブロック内のコメント(2)の部分に処理が遷移するため、「割り算に失敗しました。」が表示されます。

16-2-2 例外インスタンス

次にcatchブロックの記述についてもう少し詳しく見てみましょう。16-2-1のプログラムで0による割り算が行われると、下図のように例外インスタンスが生成されます。

例外インスタンス

例外インスタンスが生成されると処理はcatchブロックに移ります。この時catchブロックの引数部分には、発生した例外インスタンスを指すアドレスが代入されることになります。そのため、引数eを使うと例外インスタンスの持つメソッドを呼び出すことができます。例外インスタンスのもつメソッドを使うことで、例外に関する詳しい情報を得ることができます。例外を表すインスタンスがもつメソッドとしては、まずは下の2つを覚えておくと良いでしょう。

メソッド定義部 処理概要
public String getMessage() メッセージ情報を取得します。
public void printStackTrace() スタック・トレースを画面に表示します。

上記のメソッドを使用する記述に修正したCalcクラスのプログラム例を示します。

プログラム例(Calc.java)

public class Calc{
    public static void warizan(int x, int y){
        try{
            int ans = x / y;
            System.out.println(x + "÷" + y + "=" + ans + "です");
        } catch(ArithmeticException e) {
            System.out.println(e.getMessage());//(1)
            e.printStackTrace();//(2)
        }
    }
}

実行結果

/ by zero
java.lang.ArithmeticException: / by zero
        at Calc.warizan(Calc.java:4)
        at Main.main(Main.java:3)

解説
処理内容は16-2-1と同様で、0で割り算をするエラーが発生します。それによりcatchブロック内の処理に遷移します。コメント(1)の部分で実行結果1行目の内容を表示し、コメント(2)の部分で、実行結果の2行目以降が表示されています。

スタックトレース

スタックトレース(Stack Trace)は例外発生時のメソッド呼び出しの流れを示す情報です。下図のように見ていくことで、どのような順番でメソッドが発生し、どのメソッドで例外が発生したかを知ることができます。

スタックトレース

16-3 例外のスロー

ここまで例外の処理方法について学びましたが、例外は必ずしもtry-catchブロックで囲まなければならない。というわけではありません。例外のもう1つの処理方法として、例外をスロー(throw)する方法があります。

例外のスローとは、文字どおり例外を呼び出し元のメソッドに投げる(throw)ことです。まずは下記のプログラムの動作の様子を確認してみましょう。

プログラム例(Utility.java)

import java.io.FileReader;

public class Utility{
    public static void openFile(String filename){
        FileReader reader = new FileReader(filename);
    }
}

コンパイル結果

Utility.java:5: エラー: 例外FileNotFoundExceptionは報告されません。スローするには、捕捉または宣言する必要があります
                FileReader reader = new FileReader(filename);
                                    ^
エラー1個

解説
UtilityクラスのopenFileメソッドは、引数filenameで与えられた文字列をファイル名として読込用のファイルを開くメソッドです。ファイルの読込にはAPIに含まれるFileReaderクラスを使っています。FileReaderのコンストラクタはFileNotFoundExceptionという検査例外を発生させる可能性があります。そのため例外処理をしていない現在のプログラムではではコンパイルが成功しません。

このプログラムのコンパイルを成功させるためには、次の2つのうちいずれかを行わなければなりません。

  • try-catch文による例外処理を行う(16-2で紹介した方法)
  • 発生しうる例外をスローする

今回は例外をスローする方法を採用します。そのためには例外が発生しうるメソッドの宣言部分を次のように記述します。

メソッド定義 throws 例外名1, 例外名2...{

メソッド定義の最後にthrowsと記述し、その後ろに発生しうる例外を記述します。発生しうる例外が複数ある場合は、,で区切って複数記述できます。

この文法を使って、Utilityクラスを次のように書き換えます。

プログラム例(Utility.java)

import java.io.*;

public class Utility{
    public static void openFile(String filename) throws FileNotFoundException{
        FileReader reader = new FileReader(filename);
    }
}

openFileメソッドの定義部分にthrows FileNotFoundExceptionが付け加えられています。これはopenFileメソッドがFileNotFoundException例外を発生させる可能性があることを宣言しています。と同時に、例外が発生した場合にはtry-catch文による処理を行わずにそのまま例外をスローすることを意味します。

次にUtilityクラスを利用するMainクラスを作成します。プログラム例を示します。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        Utility.openFile("hello.txt");
    }
}

Utilityクラスの持つopenFileメソッドを呼び出すだけのシンプルなものです。2つのファイルを修正したらMain.javaをコンパイルしてみましょう。すると次のエラーが発生します。

Main.java:3: エラー: 例外FileNotFoundExceptionは報告されません。スローするには、捕捉または宣言する必要があります
                Utility.openFile("hello.txt");
                                ^
エラー1個

エラーの発生個所がMain.javaの3行目となっています。これは、UtilityクラスのopenFileメソッドでthrows宣言をしたことにより、例外の処理責任がmainメソッドに移動したことを意味します。したがって、Mainクラスを次のように修正します。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        try{
            Utility.openFile("hello.txt");
        } catch(Exception e) {
            System.out.println("ファイルを開けません。");
        }
    }
}

最後にコンパイル・実行をしてみましょう。Utility.javaとMain.javaをコンパイルし、Mainクラスを実行します。※実行する際は、カレントフォルダ内にhello.txtという名前のファイルを置かないようにしてください。

実行結果

ファイルを開けません。

実行時の動作の様子を下図に示します。

throwとcatch

Utility.javaの5行目で例外が発生していますが、openFileメソッドにはthrowsが宣言されているため、発生した例外は呼び出し元であるmainメソッドに投げられます。mainメソッド側では例外が発生したと判断されるため、catchブロックによって例外処理が行われます。その結果「ファイルを開けません。」と画面に表示されます。

16-4 例外クラスの作成

16-4-1 例外クラスの定義

ここでは例外クラスを自作する方法について学びます。Exceptionクラスを継承したクラスを定義することで、例外クラスを自作することが可能です。プログラム例を示します。

プログラム例(ParameterException.java)

public class ParameterException extends Exception{
    public ParameterException(){
        super("引数が不正です。");
    }
}

解説
Exceptionクラスを継承していますので例外クラスとして利用できます。コンストラクタ内では、親クラスのコンストラクタを呼び出しています。このコンストラクタはString型の引数を1つとり、その引数の値を例外発生時のメッセージとして設定してくれます。

16-4-2 例外クラスの利用

次に自作した例外クラスの利用例を示します。

(Utility.java)

public class Utility{
    public static int calcAge(int year) throws ParameterException{
        int nowYear = 2024;
        if(year > nowYear){
            throw new ParameterException();
        }
        return nowYear - year;
    }
}

解説
calcAgeメソッドは、現在の西暦年から引数で与えられた生年を引いて年齢を返すメソッドです。ただし、引数birthYearに現在年(nowYear)より未来の値が設定された場合は、例外が発生する仕様となっています。例外を発生させるにはthrowキーワードを利用し、次のような命令文を記述します。

throw 例外インスタンス;

プログラム例では例外インスタンスにあたるものとして、インスタンスの生成文(new ParameterException())が記述されています。

これによりcalcAgeメソッドはParameterException例外を発生させうるメソッドになりましたので、メソッド宣言部のthrowsによってその事を宣言しています。

最後にこのcalcAgeメソッドを利用するMainクラスを作成し、動作を確認してみましょう。

(Main.java)

public class Main{
    public static void main(String[] args){
        try{
            Utility.calcAge(2100);
        } catch(ParameterException e) {
            System.out.println(e.getMessage());
        }
    }
}

UtilityクラスのcalcAgeメソッドはParameterException例外を発生させる可能性がありますので、try-catch文で例外処理を行っています。

ParameterException.java、Utility.java、Main.javaの3つのファイルをコンパイルし、実行してみましょう。

実行結果

引数が不正です。

実行時の動作は下図のようになります。

  • UtilityクラスのcalcAgeメソッドに引数として2100を渡して呼び出しています。
  • 2100はcalcAgeメソッドの持つ現在年の値2024より先の値となるため、例外が発生します。
  • 発生した例外はmainメソッド側にスローされ、catchブロックでキャッチされます。
  • e.getMessage()を呼び出すことでParameterExceptionクラスのコンストラクタで設定した文字列を取得し、メッセージとして表示しています。
タイトルとURLをコピーしました