【未経験からのプログラミング講座】STEP3.クラスとオブジェクト3

Pocket

3. 例外処理、スタックトレース、パッケージ

3.1. 例外処理(tryとcatch)

たとえコンパイルできたとしても、ユーザーの思いがけない操作などでプログラムの実行中にプログラムが実行不可能になるような事象が起こります。このような現象を例外(Exception)と呼びます。

3.1.1. 継承クラス間のアクセス

例外は、宣言した配列の要素を超えて配列を使用したり、0で除算したりした時に起こります。

サンプル

class test {
    public static void main(String[] args) {
        System.out.println(10 / 0);
    }
}

これを実行すると

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

というメッセージを出力し、Javaが処理を強制終了させます。処理が途中で中断しないようにするには、例外が発生したときのために特別な記述をする必要があります。まず例外が発生しているかどうかを監視するため、発生する可能性がある場所をtryで囲みます。

try {
    statements...;
}

tryブロック内のステートメントやメソッドで例外が発生するとプログラムはその例外の処理ができる処理班catchブロックを検索します。catchブロックはtryブロックの後方に記述し、例外に備えます

catch (ErrType param) {
    statements...;
}

ErrTypeには例外クラスを指定し、paramには例外のパラメータが入ります。発生した例外オブジェクトが指定した例外クラスと一致したときに始めて、そのcatchブロックが実行されます。0の除算の時にはjava.lang.ArithmeticExceptionの例外オブジェクトが発生したので、この例外オブジェクトをcatchに指定すれば、例外処理ができるようになります。

サンプル1

class test {
    public static void main(String[] args) {
        try {
            System.out.println(10 / 0);
            System.out.println("終了処理");
        } catch (ArithmeticException e) {
            System.out.println("エラー :" + e);
        }
    }
}

次にcatchは目的の例外処理に応じて任意の数だけ書けます。実行時に発生するExceptionの種類が1つだけとは限らないからです。

サンプル2(目的の例外が3つ)

class test {
    public static void main(String args[]) {
        try {
            //int型へ置換
            int cnt = Integer.parseInt(args[0]);

            System.out.println(cnt / 0);
            System.out.println("終了処理");

        } catch (ArrayIndexOutOfBoundsException e1) {
            System.out.println("エラー :" + e1);
        } catch (NumberFormatException e2) {
            System.out.println("エラー :" + e2);
        } catch (ArithmeticException e3) {
            System.out.println("エラー :" + e3);
        }
    }
}

上記の3つの例外のスーパークラスであるExceptionクラスを使用すると、このようにも書けます。

サンプル3(Exceptionクラスを使用)

class test {
    public static void main(String args[]) {
        try {
            //int型へ置換
            int cnt = Integer.parseInt(args[0]);

            System.out.println(cnt / 0);
            System.out.println("終了処理");

        } catch (Exception e) {
            System.out.println("エラー :" + e);
        }
    }
}

ただし、この書き方は発生した例外ごとに処理を行うことはできないので注意しましょう。

例外が発生した場合、例外が発生した箇所より後に記述されているtryブロックの処理は行われません。

そのため、例外の発生する箇所より後に記述されているSystem.out.println("終了処理");は実行されません。しかし、例外が発生しても必ず処理を行いたい場合が存在します。その場合は、finallyブロックを使用します。

finally {
    statements...;
}

finallyブロックに記述された処理は、エラーの発生有無に関わらず実行されます。そのため、必ず行わなければならない事後処理を記述するのに適しています。

サンプル

class test {
    public static void main(String args[]) {
        try {
             //int型へ置換
            int cnt = Integer.parseInt(args[0]);

            System.out.println(cnt / 0);

        } catch (Exception e) {
            System.out.println("エラー :" + e);
        } finally {
            System.out.println("終了処理");
        }
    }
}

こう記述することにより必ずSystem.out.println("終了処理");は実行されます。また、finallyブロックは省略可能で、必ず実行しなければいけない処理が存在しない場合は記述しなくてもかまいません。

問題 3-3-1

以下のメソッドはArrayIndexOutOfBoundsExceptionが発生します。
処理が異常終了しないよう、また記述されている処理は全て実行するように修正してみよう。

class test {
    public static void main(String args[]) {
        System.out.println("開始処理");
        String[] str1 = new String[1];
        str1[1] = "Exception Test";
        System.out.println("終了処理");
    }
}

3.2. 例外処理(throws)

今まではエラーが発生した場合try〜catchを使って処理してきました。

今回はエラーが発生した場合、発生した場所で処理をしないで、呼び出した側にエラーを通知するようにします。そうすることで具体的なエラー処理を呼び出し側に任せます。

3.2.1. 例外のthrow

サンプル

class DivideZero {
    public void happenException() throws ArithmeticException {
        System.out.println(10 / 0);
        System.out.println("終了処理");
    }
}

try〜catchがなくなって代わりにメソッド名のところに例外が記述されています。

void happenException() throws ArithmeticException {

このようにメソッドで発生する可能性のある例外をthrowsの後に記述します。throwsに指定した例外が発生するとそこで処理を中断し、呼び出し元にエラーを通知します。例外が発生した以降の行はtry〜catch同様実行されません。

次の例は先ほど作ったDivideZeroクラスのhappenException()メソッドを呼び出しています。

サンプル

class DivideZeroCall{
    public static void main(String[] args){
        DivideZero zero = new DivideZero();
        try{
            //ここで例外が発生
            //例外処理はこの中では行わない 
            zero.happenException();

        }catch(ArithmeticException e){

            //代わりに呼び出し側で例外の対応を行う。
            System.out.println("ArithmeticException発生");
        }
        System.out.println("実行されました");
    }
}

今までは、例外が発生したメソッドでtry〜catchの定義を行っていたので、呼び出し元の処理は何も記述していませんでした。今回の例では、呼び出し先のhappenException()メソッドで例外処理を行っていないので、呼び出し元の方でtry〜catchを定義する必要があります。

また、throwする例外は複数記述することもできます。その場合は、【,】カンマ区切りで記述していきます。

void happenException() throws ArithmeticException, ArrayIndexOutOfBoundsException, NumberFormatException {

例外を記述する順番は関係ありません。

この場合、呼び出し元ではArithmeticExceptionArrayIndexOutOfBoundsExceptionNumberFormatExceptionの3つの例外をcatchします。またtry〜catchの時と同じようにスーパークラスを指定すると、指定したクラスのサブクラスをすべてまかなってくれます。

サンプル(呼び出し元)

class DivideZeroCall2{
    public static void main(String[] args){
        DivideZero2 zero = new DivideZero2();
        try{
            //ここで複数の例外が発生
            //例外処理は呼び出す先のメソッド内では行われない 
            zero.happenException(args[0]);

        }catch(Exception e){
            //代わりに呼び出し元で例外の対応を行う。
            System.out.println("例外発生");
            System.out.println("エラー :" + e);
        }
        System.out.println("実行されました");
    }
}

サンプル(呼び出し先1)

class DivideZero2 {
    public void happenException (String str)
        throws ArithmeticException, ArrayIndexOutOfBoundsException, NumberFormatException {

        //int型へ置換
        int cnt = Integer.parseInt(str);

        System.out.println(cnt / 0);
    }
}

呼び出し先は次のようにも書けます。

サンプル(呼び出し先2)

class DivideZero2 {
    //Exceptionクラスでまとめる
    public void happenException (String str) throws Exception{

        //int型へ置換
        int cnt = Integer.parseInt(str);

        System.out.println(cnt / 0);
    }
}

問題 3-3-2

以下のメソッドはArrayIndexOutOfBoundsExceptionが発生します。
このメソッドを呼び出し、発生した例外に対応できるようにしてみよう。メソッドにも修正が必要です。

class ThrowsTest {
    public void ThrowTest () {
        System.out.println("開始処理");
        String[] str1 = new String[1];
        str1[1] = "Exception Test";
    }
}

3.3. スタックトレース

例外が発生した時に、printStackTraceメソッドが出力するスタックトレースを見ると、どのような過程を経て該当個所が呼び出されているかが分かります。

3.3.1. スタックトレースで処理の過程を知る

例外発生時にスタックトレースで処理の過程を知ることでソースの修正の箇所が解ります。

サンプル(DevideZeroCallクラスを修正)

class DivideZeroCall{
    public static void main(String[] args){
        DivideZero zero = new DivideZero();
        try{
            //ここで例外が発生
            //例外処理はこの中では行わない 
            zero.happenException();

        }catch(ArithmeticException e){

            //代わりに呼び出し側で例外の対応を行う。
            e.printStackTrace();
        }
    }
}

動作時の出力内容

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at DivideZero.happenException(DevideZero.java:3)
	at DivideZeroCall.main(DivideZeroCall.java:7)

上記はプログラムを実行した際に出力されたエラー情報のスタックトレースです。この出力された結果を下から上へ順に見ていくことで、どのような順番で処理が実行され、どのメソッドがエラーを発生させたのか解ります。

at DivideZeroCall.main(DivideZeroCall.java:7)

最初にDevideZeroCallクラスのmainメソッドが実行されました。

at DivideZero.happenException(DevideZero.java:3)

次にDevideZeroクラスのhappenExceptionメソッドが実行されました。

Exception in thread "main" java.lang.ArithmeticException: / by zero

最後に今回の実行で発生したExceptionの種類となります。今回はで除算を行った際に発せられるjava.lang.ArithmeticExceptionが発生しました。そのため、直前のDevideZeroクラスのhappenExceptionメソッドでExceptionが発生したことが解ります。

このようにExceptionが発生した際にどの箇所でどのようなExceptionが発生したかを容易に調べる事が出来ます。このため、スタックトレースはException発生時のソース修正を行う上で、必要不可欠な情報だと言えます。

3.3.2. System.out

例外処理でExceptionが発生した際にデバッグを行うための情報スタックトレースを説明しましたが、実際の運営ではExceptionが発生しないバグというものが多々存在します。例えば、データベースに更新されるべき値が異なる。分岐等で実際に通って欲しいロジックに通らない。というのが良くあるパターンです。これは引数として渡す値が異なっていることで発生したり、計算式が間違っていたりします。この場合はどのようにデバッグを行えば良いのでしょうか?

その場合はSystem.out.print() / System.out.println()を使用することにより瞬間の値をプロンプト上に表示させる事が出来ます。

System.out.print(Object)	←表示後、改行されない。
System.out.println(Object)	←表示後、改行される。

これを使用する事により、処理の各々の箇所でどの値が参照されているのか調べる事が可能です。System.outを使用しないとデバッグするうえで開発者が取得出来る情報は限られていますし、また取得出来たとしても必要な情報かどうかは分からないため、Exceptionが発生しないケースでのデバッグでは必要な情報を得る手段として必要です。

3.4. パッケージ

プログラムの数が増えてくると、管理も大変になってきます。一般的にファイルが増えてきた場合、どのようにすると管理しやすいかというと、フォルダに分けてしまえば分かりやすくなります。

Javaでも同じようにプログラムをフォルダに分けて管理することが出来ます。Javaの場合このフォルダを「パッケージ」と呼びます。

3.4.1. パッケージ化による利点

(1) クラスの分類

クラスが大量に存在すると、プログラマーは必要なクラスを見つけにくくなります。そこで、互いに関連するクラス同士をパッケージとしてまとめることによって、必要なクラスを簡単に探し出せるようにします。

例えば、Javaの標準クラスライブラリでは、ファイルの読み書きなどのためのクラスはパッケージjava.ioに、ネットワーク関連のクラスはパッケージjava.netに、というように機能別に分かりやすくまとめてあります。

(2) クラス名の衝突を防ぐ

Javaは名前をもとにクラスを読み込むため、同じ名前が存在すると、間違ったファイルを読み込んでしまってエラーが発生する可能性が存在します。開発において名前がかぶらないようにするのは困難です。このとき、同じ名前のクラスを異なったパッケージに置くことにより、問題を解決することが出来ます。

この仕組みは複数メンバーで共同開発を行う場合も有効です。Aさんの担当する package-a パッケージと、Bさんの担当するpackage-b パッケージというようにパッケージを分けていれば、AさんとBさんが、Function という同じ名前のクラスを作っても大丈夫です。AさんとBさんは、それぞれ自分の担当するパッケージの中でクラス名が重複しないように気をつければよいのです。

(3) アクセス制限

パッケージを利用することで、同じパッケージ中のクラスからはアクセスできるが、ほかのパッケージ中のクラスからはアクセスできないようなメソッドやメンバ変数を定義したり、ほかのパッケージからアクセスできないようなクラスを作成したりすることができます。

3.4.2. パッケージの使用

(1) パッケージの宣言

FunctionAと言う名前のクラスをpackageAと言う名前のフォルダに作ってみましょう。

サンプル

package packageA;

class FunctionA {
    public void showMessage () {
        System.out.println("パッケージの使用");
    }
}

ここで注意することは、このプログラムがpackageAパッケージに中にあることが分かるようにpackage packageAと宣言する必要があります。構文は【pacakge パッケージ名】となり、必ずクラスの一番最初に記述します。また、パッケージの内部に別のパッケージを宣言することも可能です。下の例はpackageAの内部にsubPackageというパッケージを宣言する時に記述します。

package packageA.subPackage;

複数の階層になってパッケージが存在する場合は、パッケージ間を【.】(ピリオド)で区切り、宣言を行います。

(2) パッケージの呼び出し

次に(1)で宣言したパッケージを同一階層のpackageBパッケージのFunctionBクラスから呼び出してみましょう。

サンプル

package packageB;
import packageA.FunctionA;
class FunctionB {
    public static void main(String[] args) {
        FunctionA funcA = new FunctionA();
        funcA.shouMessage();
    }
}

ここで注意することは、呼び出したいFunctionAクラスが別のパッケージに存在するので、クラスを使用するにはパッケージ毎宣言してあげなければいけません。

import packageA.FunctionA;

と宣言しなければ使用することが出来ません。

また、同じパッケージ内のクラスを複数使用したい時には、個別で宣言することも可能ですが、一つの記述で、複数のクラスを使用できるように宣言することも可能です。

下の例はpackageAの内部に存在する複数のクラスを使用するときに記述します。

import package.*;

複数のクラスを使用する場合は、使用したいパッケージのあとに【*】(アスタリスク)で宣言します。

問題 3-3-4

パッケージ、サブパッケージに存在するPrint/SubPrintクラスを作成し、Mainクラスで呼び出し使用ししてみよう。

|─Main.java
|─Package
  |─Print.java