メソッドとクラス

次のプログラムを考えてみましょう。

import java.io.*;
class Test {
    public static void main(String[] args) throws IOException {
        BufferedReader r =
            new BufferedReader(new InputStreamReader(System.in), 1);
                                      // データ入力の準備
        System.out.print("あなたは何歳ですか? ");
        System.out.flush();           // 強制出力
        String s = r.readLine();      // 文字列の入力
        int a = Integer.parseInt(s);  // 整数に変換
        System.out.print("初恋は何歳でしたか? ");
        System.out.flush();           // 強制出力
        s = r.readLine();             // 文字列の入力
        int b = Integer.parseInt(s);  // 整数に変換
        System.out.println("あれから " + (a - b) + " 年たちましたね");
    }
}

これは完璧なプログラムですが,メッセージを出力して整数を入力する部分が繰り返し現れています。 同じようなところは,まとめて「メソッド」として定義するのが賢い方法です。 メソッドを使ってまとめると次のようになります。

import java.io.*;
class Test {
    static int getInt(String s) throws IOException {
        BufferedReader r =
            new BufferedReader(new InputStreamReader(System.in), 1);
                                      // データ入力の準備
        System.out.print(s);          // メッセージ出力
        System.out.flush();           // 強制出力
        String t = r.readLine();      // 文字列の入力
        return Integer.parseInt(t);   // 整数に変換して返す
    }
    public static void main(String[] args) throws IOException {
        int a = getInt("あなたは何歳ですか? ");
        int b = getInt("初恋は何歳でしたか? ");
        System.out.println("あれから " + (a - b) + " 年たちましたね");
    }
}

メッセージを出力し,整数を入力する部分を,getInt というメソッドにまとめました。

じつは main も一つのメソッドです。 上のプログラムは,maingetInt の二つのメソッドを含むことになります。

メソッド getInt の頭に付いている static は,main にも付いている語ですね。 この意味は,「このメソッドはコンパイルした時点で作られる」という意味でした。

その後の int は,getIntint 型の値を返すことを意味します。 これに対して,main メソッドに付いている void という語は,main メソッドが何も値を返さないことを意味します。

getInt(String s)String s は,この getInt というメソッドが,文字列(ストリング)を受け取ることを意味します。 受け取った文字列は s という変数に代入されます。

この文字列 s のようにメソッドが受け取るもののことを,メソッドの引数(ひきすう)といいます。

メソッドを使うことを,メソッドを「呼び出す」ともいいます。 ここでは

        int a = getInt("あなたは何歳ですか? ");

のように,"あなたは何歳ですか? " という文字列を引数としてメソッド getInt() を呼び出しています。 このメソッドは,キーボードからの入力を整数に直して,その値を返します。 メソッドが返す値のことを戻り値(もどりち)などといいます。

この getInt() メソッドの中では,キーボードから入力する部分がありますが,ここでは入出力の例外(I/O Exception)が生じる可能性があります。 例外とは,いわゆるエラーのことです。 エラーが生じたときは,自前でエラー処理することもできますが,エラー処理を他に任せてしまうこともできます。 このための宣言が throws IOException でした。

throws IOException は,このメソッドの中で入出力エラーが生じたなら,それをそのまま呼び出し側に投げる(throw する)ことを意味します。 つまり, main メソッドから getInt メソッドを呼び出したとき,getInt メソッド内で生じた入出力エラーは main メソッドにそのまま投げられるので,main メソッドもまた入出力エラーを投げる必要があります。 最後に入出力エラーを受け取ってエラーメッセージを発するのは,Javaインタープリタです。

エラーを投げないで,メソッド内で自前で処理してしまうことも可能です。 それには,次のようにします。

import java.io.*;
class Test {
    static int getInt(String s) {
        DataInputStream d = new DataInputStream(System.in);
                                      // データ入力の準備
        System.out.print(s);          // メッセージ出力
        System.out.flush();           // 強制出力
        try {                         // トライする
            return Integer.parseInt(d.readLine());
        } catch (IOException e) {     // 入出力の例外なら
            System.err.println("I/O エラーです");
            return 0;                 // 仮に 0 を返す
        }
    }
    public static void main(String[] args) {
        int a = getInt("あなたは何歳ですか? ");
        int b = getInt("初恋は何歳でしたか? ");
        System.out.println("あれから " + (a - b) + " 年たちましたね");
    }
}

readLine() のような入出力メソッドを用いるときは,必ず throwscatch を使わないと,コンパイラに叱られます。

throws でエラーを他に転嫁せず,catch でエラーを捕まえれば,エラーの連鎖は断ち切れ,他でエラー処理する必要はなくなります。

実際には入出力エラーより,Integer.parseInt() メソッドで文字列が整数に変換できないというエラーが起きることのほうが多いと思います。 入出力エラーのように実際に入力装置にエラーが発生したときのエラーは絶対に throwscatch で処理しなければなりません。 これに対して,文字列が整数に直せないといったような,いわゆる「実行時のエラー」は,throwscatch で処理する必要はありませんが,処理してもかまいません。 処理するには次のようにします。

        try {                         // トライする
            return Integer.parseInt(d.readLine());
        } catch (IOException e) {     // 入出力の例外なら
            System.err.println("入出力エラーです");
            return 0;                 // 0 を返す
        } catch (NumberFormatException e) {  // 数値形式の例外なら
            System.err.println("数値形式エラーです");
            return 0;                 // 0 を返す
        }

これで,数の形式が変なときも 0 を返すようになります。

もっとも,throwscatch も必要のない例外は他にもあります。 そこで,数値形式の例外以外はすべてまとめて処理してしまうこともできます。

        try {                                // トライする
            return Integer.parseInt(d.readLine());
        } catch (NumberFormatException e) {  // 数値形式の例外なら
            System.err.println("数値形式エラーです");
            return 0;                        // 0 を返す
        } catch (Exception e) {              // それ以外の例外なら
            System.err.println("エラー: " + e);
            System.exit(1);                  // 終了する
        }

クラスを独立させる

さきほどの数値入力の例で,今度はメソッドだけでなく,クラスを独立させてしまいましょう。

それから,ついでですので整数入力ではなく,より一般性のある実数(double 型)の入力にしてみました。 また,数値形式の例外の場合は,何度でも聞き直すようにしました。

import java.io.*;

class GetNumber {
    BufferedReader r;

    GetNumber() {  // GetNumber クラスを初期化するメソッド
        r = new BufferedReader(new InputStreamReader(System.in), 1);
    }

    double get(String msg) {            // get というメソッドを作る
        double x = Double.NaN;          // NaN(非数)に設定
        while (Double.isNaN(x)) {       // NaN なら繰り返す
            try {                       // トライする
                System.out.print(msg);  // メッセージ出力
                System.out.flush();     // 強制出力
                x = Double.valueOf(r.readLine()).doubleValue();
            } catch (NumberFormatException e) {  // 数値形式の例外なら
                System.err.println("数値形式エラーです");
                x = Double.NaN;         // 非数にする→繰返し
            } catch (Exception e) {     // それ以外の例外なら
                System.err.println("エラー: " + e);
                System.exit(1);         // 終了
            }
        }
        return x;
    }
}

class Test {
    public static void main(String[] args) {
        GetNumber g = new GetNumber();  // GetNumber クラスを使う
        double a = g.get("あなたは何歳ですか? ");
        double b = g.get("初恋は何歳でしたか? ");
        System.out.println("あれから " + (a - b) + " 年たちましたね");
    }
}

これ全体を Test.java というファイルに書き込んで,コンパイルすると,Test.class だけでなく,GetNumber.class というクラスファイルもカレントディレクトリにできます。 実行するには,以前と同様に,

    java Test

とします。

このプログラムの get() メソッドは,Double.NaN という変な値を使っています。 この NaN は Not-a-Number(非数)の意味で,double 型や float 型に用意されている特別な値です。 エラーや未入力を表すのに便利です。

double 型を使っているので,答えが「12.0年」となったりして,少し変ですね。 もし double 型でなく int 型が欲しい場合は,次のようにして,(int) を付けて型変換します。

class Test {
    public static void main(String[] args) {
        GetNumber g = new GetNumber();  // GetNumber クラスを使う
        int a = (int) g.get("あなたは何歳ですか? ");  // 型変換
        int b = (int) g.get("初恋は何歳でしたか? ");  // 型変換
        System.out.println("あれから " + (a - b) + " 年たちましたね");
    }
}

この (int) のように,型の名前をかっこに入れて付けて型を変換することを,キャストする(cast)といいます。

ファイルを分ける

ここまでくれば,もうモジュールごとにファイルを分けるほうが簡単でしょう。

ファイル GetNumber.java には

   import java.io.*;

と,GetNumber クラス全体を書き込んでおきます。

Test.java には Test クラス全体を書き込んでおきます。

これらを別々にコンパイルします。

javac GetNumber.java
javac Test.java

実行は,以前と同様に,

java Test

のようにします。

自分のクラス用のディレクトリを作る

通常,自分の作ったクラスはカレントディレクトリに置いておきます。 別の場所,たとえば C:\myclasses というディレクトリにある Test.class などを使うには,

java -classpath C:\myclasses Test

とします。 カレントディレクトリ(. で表します)にも必要なクラスがある場合には

java -classpath .;C:\myclasses Test

のように ;(セミコロン)で区切って指定します。

いつも同じ場所を使うなら,環境変数 CLASSPATH を使って,

set CLASSPATH=.;C:\myclasses

のような設定を AUTOEXEC.BAT などに書き込んでおいてもかまいません。

パッケージを作る

以下はまだ古いままです

カレントディレクトリ(または -classpath オプションや CLASSPATH 環境変数で指定したディレクトリ)の中に,自分(okumura)が作った小道具(ユーティリティ)類は okumura というサブディレクトリの中の util というサブディレクトリに入れる,というように分類しておくと便利です。

たとえば GetNumber.class というユーティリティクラスが C:\myclasses\okumura\util の中にあるとして,クラスパスが C:\myclasses に指定されているとしましょう。

このとき,さきほどの GetNumber.java の先頭(import のさらに前)に,

package okumura.util;

という行を追加しておきます。 さらに,class GetNumber の前や, 外部から使うメソッドの定義の前に, public という形容詞を追加します。 変わった行だけ書き出せば,次のようになります。

package okumura.util;     // okumura.util パッケージの一員とする

import java.io.*;

public class GetNumber {  // パッケージ外から使えるように public に
    private BufferedReader r;       // 外から使わないので private に

    public GetNumber() {            // 外から使うので public に
        ...                         // 中身は同じ
    }

    public double get(String s) {   // 外から使うので public に
        ...                         // 中身は同じ
    }
}

こうしてから,コンパイルし直し,できた GetNumber.class をさきほどの C:\myclasses\okumura\util の中に移動しておきます。

★ ソースファイル GetNumber.java はカレントディレクトリに置いてコンパイルし,できた GetNumber.class を後で移動してもかまいませんし,最初から GetNumber.javaC:\myclasses\okumura\util に置いておき,パス指定付きで

javac C:\myclasses\okumura\util\GetNumber.java

のようにコンパイルしてもかまいません。 class ファイルは java ファイルと同じディレクトリに作られます。

さらに, GetNumber.class を使う側の Test.java の先頭には,

import okumura.util.GetNumber;

という行を追加します。あるいは

import okumura.util.*;

でもかまいません。

なお,この okumura というのは私の名前ですが,もっと広い範囲で使ってもらうことを考えた場合,パッケージ名が他と重ならないように,

JP.ac.matsusaka_u.okumura.util

のようにインターネットのドメイン名を逆にする方式が提案されています(残念ながらドメイン名に含まれるマイナスはJavaのパッケージ名に含めることができないのでアンダーバーに直しました)。 もっとも,これではわずらわしいので,たとえばNetscape社なら netscape.何々 というように,自社名で始まるパッケージ名もあるようです。


奥村晴彦

Last modified: 2005-09-22 15:42:17