例外処理とキャラクタストリーム

例外処理

次のプログラムについて考えてみる。

package j2.lesson08.example;

import java.io.*;

public class MalformedInteger {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        // コンソールから入力を 1 行分だけ読み込む
        System.out.print("input>");
        String input = reader.readLine();
        System.out.println("input = " + input);
        
        // 入力された値を int 型に変換する
        int n = Integer.parseInt(input);
        System.out.println("input * 10 = " + (n * 10));
    }
}

上記のプログラムは、コンソールから入力した整数の値と、その10倍の値を表示するだけの単純なものである。このプログラムを実行し、365 と入力すると次のように表示される。

input>365
input = 365
input * 10 = 3650

このプログラムは、コンソールから入力される値は原則として整数の形をとることを想定している。もしここで原則から外れた入力、つまり整数でない「abc」といった入力をした場合どうなるか。試してみると次のように表示される。

input>abc
input = abc
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Integer.parseInt(Integer.java:447)
    at java.lang.Integer.parseInt(Integer.java:497)
    at j2.lesson08.example.MalformedInteger.main(MalformedInteger.java:15)

上記のように、「input = abc」というところまでは表示されているが「input * 10 = ???」という部分が表示されず、代わりにエラーメッセージが表示された。

Integer.parseInt(String) は、整数の形をした文字列を引数に受け取って、その文字列が表す数値を返すメソッドである (これまでに何度も使用してきた)。原則として整数の形をした文字列しか扱わないため、「abc」のような整数の形をしていない入力は原則から外れた例外的な入力である。このような例外が発生すると、Java ではプログラムの実行を強制的に終了し、例外が発生したことをコンソールに表示させる。この例では、次のような例外のメッセージ表示された。

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Integer.parseInt(Integer.java:447)
    at java.lang.Integer.parseInt(Integer.java:497)
    at j2.lesson08.example.MalformedInteger.main(MalformedInteger.java:15)

まず 1 行目の「java.lang.NumberFormatException」というのは、発生した例外の種類である。これはクラスの名前で、Java API 仕様にもしっかり書かれている。

このクラスの概要を調べると、「アプリケーションが文字列を数値型に変換しようとしたとき、文字列の形式が正しくない場合にスローされます。 」と書かれている (「スローされる」という表現はここでは「発生する」と読み替えればよい)。

また、1 行目の次の部分「For input string: "abc"」は、例外発生時のメッセージであり、日本語では「入力された文字列 "abc" に対して」と訳せる。まとめると、「入力された文字列 "abc" を数値型に変換しようとしたとき、文字列の形式が正しくなかった」ということが分かる。

2 行目以降は、例外が発生した箇所がどこから呼び出されているかという情報を表示している (スタックトレース情報と呼ばれる)。特に最後の行に注目してみると、「MalformedInteger.main(MalformedInteger.java:15)」という情報から、「MalformedInteger クラスの main メソッド (MalformedInteger.java ファイルの 15 行目)」という情報が分かる。その手前の行が Integer.parseInt であるため、MalformedInteger.java ファイルの 15 行目から Integer.parseInt を呼び出した結果、例外が発生していると推測できる。

このような例外は Integer.parseInt に限ったものではない。他にも配列を扱う際に、範囲外のインデックスにアクセスした場合にも例外が発生する。

package j2.lesson08.example;

public class ArrayIndex {

    public static void main(String[] args) {
        // 長さ 3 の配列
        int[] array = new int[3];
        array[0] = 10;
        array[1] = 20;
        array[2] = 30;
        
        System.out.println("array[0] = " + array[0]);
        System.out.println("array[1] = " + array[1]);
        System.out.println("array[2] = " + array[2]);
        
        // 配列の範囲外にアクセス
        System.out.println("array[3] = " + array[3]);
    }
}

上記プログラムを実行すると、次のように表示される。

array[0] = 10
array[1] = 20
array[2] = 30
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
	at j2.lesson08.example.ArrayIndex.main(ArrayIndex.java:17)

その他これまでに扱ったプログラムの中で、例外が発生する状況は以下のような物が挙げられる。

例外の捕捉

例外が発生した際に、プログラムを終了させるのではなくそれぞれの例外に対処するような処理を書くことができる。

package j2.lesson08.example;

import java.io.*;

public class MalformedInteger2 {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        System.out.print("input>");
        String input = reader.readLine();
        System.out.println("input = " + input);

        try {
            int n = Integer.parseInt(input);
            System.out.println("input * 10 = " + (n * 10));
        }
        catch (NumberFormatException e) {
            // 例外が発生したら、try の実行を中断してここが実行される
            System.out.println(input + "は数値ではありません");
        }
    }
}

上記プログラムを実行し「abc」と入力すると、長いエラーメッセージが表示されるのではなく「abcは数値ではありません」とだけ表示されてプログラムが終了する。これまでは Integer.parseInt(String) に整数を表していない文字列を渡すと NumberFormatException という例外が発生してプログラムが終了していたが、ここでは発生した例外を捕捉し、適切なメッセージを表示している

プログラム中に出てくる try や catch は次のようなものである。

try {
    正常処理
}
catch (例外クラス e) {
    例外処理
}

上記のように書くと、まず「正常処理」の部分が実行され、そのまま例外が発生せずに「正常処理」の部分が完了すれば「例外処理」の部分を実行せずに try-catch 以降のプログラムを続けて実行する。「正常処理」の部分で例外が発生した場合、「正常処理」をそこで打ち切って「例外処理」を実行する

「例外クラス」の部分には捕捉する例外クラスの名前を記述する (Integer.parseInt ならば NumberFormatException)。

また、「try { ~ 正常処理 ~ }」の部分を try 節、「catch ~ { ~ 例外処理 ~ }」の部分を catch 節と呼ぶ。try 節で発生した例外を catch する場合、発生する例外の種類と同じ例外クラスを捕捉できる catch 節を用意する必要がある。例えば、次のように書いても catch 節は意味を成さない。

try {
    // NumberFormatExcetpion が発生する可能性がある
    n = Integer.parseInt(input);
}
// 算術例外を捕捉できる catch 節
catch (ArithmeticException e) {
    ...
}

try 節で発生した例外を捕捉すると、「catch (NumberFormatException e) 」のように例外クラスの右に書いた e という変数が自動的に作成され、その変数に例外の情報が格納される。この e の部分はどのような名前でもよく、例えば catch (NumberFormatException nfe), catch (NumberFormatException exc) などと書いてもよい。この変数の型は指定した例外クラスのインスタンスと同じ型になり、通常のインスタンスとして扱うことができる。

先ほどのプログラムを「数値以外の値が入力されたら、0 が入力されたことにして計算を行う」という処理に書き直す場合、次のように書けばよい。

// 変数 n は try の外側でも使うのでここで宣言
int n;
try {
    n = Integer.parseInt(input);
}
catch (NumberFormatException e) {
    n = 0;
}
System.out.println("input * 10 = " + (n * 10));

一つの try 節に対して、2 つ以上の catch 節を書くこともできる。

try {
    // NumberFormatException が発生する可能性がある
    int a = Integer.parseInt(input1);
    int b = Integer.parseInt(input2);

    // ArithmeticException が発生する可能性がある (b == 0 のとき)
    int c = a / b;
    System.out.println("a / b = " + c);
}
catch (NumberFormatException e) {
    // 数値以外が入力された場合の処理
}
catch (ArithmeticException e) {
    // 割る数に 0 が入力された場合の処理
}

上記のように書くと、一つ目の catch 節 (NumberFormatException) では「数値以外が入力された場合の処理」を記述することができ、二つ目の catch 節 (ArithmeticException) では「割る数に 0 が入力された場合の処理」を記述することができる。

その他、例外についての詳しい内容は使っていく中で説明していく。

テキストファイルの作成

プログラムで作成したデータは、コンソールに表示する以外にどこか別のファイルに書き出すこともできる。今回は、扱いが簡単なテキストファイル (文字や改行からのみなるファイル) を作成する。

テキストファイルを作成する場合、java.io.FileWriter というクラスを使用すると便利である。

例えば、U:\hello.txt というファイルを作成し、その内容を

Hello, world!
こんにちは、世界!

としたい場合、次のように書ける。

package j2.lesson08.example;

import java.io.FileWriter;
import java.io.IOException;

public class HelloWriter {
    public static void main(String[] args) throws IOException {
        
        // 場所を指定してファイルを開く。場所を指定する際 \ は / に置き換える
        FileWriter file = new FileWriter("U:/hello.txt");
        
        // 開いたファイルに文字列を書き込む
        // \n は改行を表す
        file.write("Hello, world!\n");
        file.write("こんにちは、世界!\n");
        
        // ファイルを閉じる
        file.close();
    }
}

最初の

FileWriter file = new FileWriter("U:/hello.txt");

では、U:\hello.txt というファイルを作成し、このファイルに書き込みができる状態にするためにファイルを開いている。U:\hello.txt ではなく U:/hello.txt としているのは、\ (Back Space の左付近にある¥) という文字列を使う場合にいくつか問題点があるため、同じ意味を持つ / を使っている。

次の

file.write("Hello, world!\n");
file.write("こんにちは、世界!\n");

では、開いたファイルに文字列を書き込んでいる。文字列はファイルの先頭から順番に書き込まれていき、上記の2行によって

Hello, world!
こんにちは、世界!

という内容の文字列をファイル U:\hello.txt に書き込むことができる。\n は改行を表している。

最後に

file.close();

を行って、書き込み用に開いたファイルを閉じている。

java.io.FileWriter での例外の扱い

ファイルなどへの入出力を行う場合、例外処理を多数行わなければならない。例えば、次のような例外的な場面が考えられる。

入出力に関する例外を表すクラスとして、java.io.IOException というクラスが存在する。コンソール入力をする際にもこのクラスを使っていた。

先ほどの java.io.FileWriter クラスでは、new によってインスタンスを生成したり、write によって文字列を書き出したり、close によってファイルを閉じたりするそれぞれの場面で java.io.IOException が発生する可能性がある。この例外の扱い方について解説する。

throws の指定

最も単純な例外の扱い方は、throws という指定をメソッド (やコンストラクタ) の宣言部分につけることである。仮引数リストの直後に「throws 例外クラス」と記述する。

public static void main(String[] args) throws IOException {

throws の指定をすると、このメソッドを実行中に例外が発生した場合に、このメソッドの実行を中止して呼び出し元に例外処理を任せることができる。main メソッドは通常 Java から直接呼び出されるため、main メソッドで例外処理を行わないとエラーが表示されてプログラム全体が終了する。

もし、この IOException を捕捉して例外処理を行いたい場合、throws ではなく try 節によって例外が発生する箇所を囲み、catch 節で適切な例外処理を書く。例えば、ファイルを書き込むことに失敗した場合にメッセージを表示するには次のように書けばよい。

FileWriter file = new FileWriter("U:/hello.txt");
try {
    file.write("Hello, world!");
}
catch (IOException e) {
    System.out.println("ファイルに書き込めませんでした。" + e.getMessage());
}

throws の利点は、例外を処理することを自分では行わずに呼び出し元にまかせることができるという点である。java.io.FileWriter の write(String) メソッド (java.io.Writer を継承している) を見てみると、次のような記述がある。

例外:
IOException - 入出力エラーが発生した場合

java.io.FileWriter がファイルに文字列を書き込めないことが分かっても、java.io.FileWriter にはどうしていいのか分からない。例えばあるプログラムでは違うファイルを開こうとするかもしれないし、他のプログラムではあきらめてプログラムを終了するかもしれない。これら全ての条件を java.io.FileWriter が網羅するのは不可能である。このような場合は java.io.FileWriter が「ファイルに書き込めなかった」という例外を呼び出し元に通知し、ファイルに書き込めなかった際の例外処理を呼び出し元に記述してもらう

また、throws の後にカンマ区切りでいくつも例外クラスを指定することもできる。

try-catch-finally

ファイルを開いた場合は必ず閉じる必要がある。本演習のように実行時間が短いプログラムを扱う際にはそれほど問題にならないが、一年中実行し続けるようなプログラムでファイルを開いたままにしておくと、開けるファイルの個数の限界に達してファイルが開けない状態になる可能性がある。

先ほどの例を見てみよう。

// 場所を指定してファイルを開く。場所を指定する際 \ は / に置き換える
FileWriter file = new FileWriter("U:/hello.txt");

// 開いたファイルに文字列を書き込む
// \n は改行を表す
file.write("Hello, world!\n");
file.write("こんにちは、世界!\n");

// ファイルを閉じる
file.close();

もし、file.write を実行中に例外が発生した場合、file.close を呼び出す前にこのメソッドを終了してしまいファイルが閉じられない。そこで、どのような例外が発生してもファイルを閉じられるように書き換えてみる。

package j2.lesson08.example;

import java.io.FileWriter;
import java.io.IOException;

public class HelloWriter2 {
    public static void main(String[] args) throws IOException {
        // 開く際に失敗しても、まだ開いていないので閉じる必要はない
        FileWriter file = new FileWriter("U:/hello.txt");
        try {
            file.write("Hello, world!\n");
            file.write("こんにちは、世界!\n");
        }
        catch (IOException e) {
            System.out.println("ファイル書き込み中にエラーが発生");
            // 例外が発生してもファイルを閉じる
            file.close();
            return;
        }
        
        // 正常に終了してもファイルを閉じる
        file.close();
    }
}

file.close() がプログラム中に2箇所入ってしまっている。これは、正常にファイルに書き込めた場合と例外が発生した場合の両方でファイルを閉じる操作を入れているためである。

Java では、「正常に終了しても例外が発生しても必ず最後に一度だけ実行される処理」というものを記述できる。

これは、try-catch 節を書いた後に、finally 節という終了処理を記述するブロックを書くことにより実現できる。

try {
    // 正常処理
}
catch (例外クラス1 e) {
    // 例外処理 (例外クラス1)
}
catch (例外クラス2 e) {
    // 例外処理 (例外クラス2)
}
...
finally {
    // 終了処理
}

この処理順序は、次の図で表される。


また、try-catch-finally の catch 節は 0 個でもよい。その場合は、正常に終了した場合と、捕捉できない例外が発生した場合のどちらでも finally 節が実行されることになる。

先ほどのプログラムを、finally を使って書くと次のように書ける。

package j2.lesson08.example;

import java.io.FileWriter;
import java.io.IOException;

public class HelloWriter3 {
    public static void main(String[] args) throws IOException {
        FileWriter file = new FileWriter("U:/hello.txt");
        // 正常処理
        try {
            file.write("Hello, world!\n");
            file.write("こんにちは、世界!\n");
        }
        // 例外処理
        catch (IOException e) {
            System.out.println("ファイル書き込み中にエラーが発生");
        }
        // 終了処理
        finally {
            // 正常に終了しても例外で終了しても、必ずファイルを閉じる
            file.close();
        }
    }
}

正常処理、例外処理 (異常処理)、終了処理の3つのパートに分けて、読みやすいプログラムを書けるようになる。上記のように書いても throws IOException が必要なのは、new FileWriter(String) と file.close() が IOException を発生させるからである。この throws 指定も消したければ、次のように書き換えればよい。

FileWriter file;
try {
    file = new FileWriter("U:/hello.txt");
}
catch (IOException e) {
    System.out.println("ファイルが開けませんでした");
    // 開けなかったので閉じる必要はない
    return;
}
// 正常処理
try {
    file.write("Hello, world!\n");
    file.write("こんにちは、世界!\n");
}
// 例外処理
catch (IOException e) {
    System.out.println("ファイル書き込み中にエラーが発生");
}
// 終了処理
finally {
    try {
        file.close();
    }
    catch (IOException e) {
        // ファイルを閉じるのに失敗した場合の例外処理
    }
}

FileWriter でファイルを書き込む場合のパターン

ファイルを開くような処理をする場合、必ず下記のように書くとよい。

(ファイルを開く処理)
try {
    (ファイルを扱う処理)
}
... (必要なだけ catch 節)
finally {
    (ファイルを閉じる処理)
}

このパターンに当てはめると、FileWriter を使った処理を行う場合には次のどちらかを使うとよい。

まず、発生した入出力例外 (java.io.IOException) を呼び出し元に任せるには、throws で IOException を指定し、catch 節を書かない。これによって、ファイル書き込み中に例外が発生しても確実にファイルを閉じることができ、さらにその例外処理を呼び出し元に任せることができる。

public void fileWriterSample1() throws IOException {
    // ファイルを開く処理
    FileWriter writer = new FileWriter("filename.txt");
    try {
        // ファイルを扱う処理
        writer.write("Hello, world!\n");
        writer.write("こんにちは、世界!\n");
    }
    finally {
        // ファイルを閉じる処理
        writer.close();
    }
}

main メソッドで throws を指定すると、その例外が発生した際にプログラムを強制終了させることができる。

また、発生した入出力例外 (java.io.IOException) を同じメソッド内で捕捉するには、try-finally の間に catch 節を書き、そこに例外処理を記述する。これによって、発生した例外に対して正しく対応することができ、例外処理の終了後にファイルを確実に閉じることができる。

public void fileWriterSample2() {
    // ファイルを開く処理
    FileWriter writer;
    try {
        writer = new FileWriter("filename.txt");
    }
    catch (IOException e) {
        // ファイルを開く処理で失敗した場合
        System.out.println("filename.txtが開けませんでした");
        return;
    }
    try {
        // ファイルを扱う処理
        writer.write("Hello, world!\n");
        writer.write("こんにちは、世界!\n");
    }
    catch (IOException e) {
        // ファイルを扱う処理で失敗した場合
        System.out.println("filename.txtへ書き込み中に例外が発生しました");
        return;
    }
    finally {
        // ファイルを閉じる処理
        try {
            // 面倒だが、writer.close() も例外を発生するので捕捉しなければならない
            writer.close();
        }
        catch (IOException e) {
            System.out.println("filename.txtを閉じる際に例外が発生しました");
        }
    }
}

ただし、FileWriter のコンストラクタ (new FileWriter(String)) や、close() メソッドも例外を発生させる可能性があるため、throws を消すには それぞれの部分で try-catch により適切な例外処理を行う必要がある

前者と後者を組み合わせてプログラムを書いてもよい。

テキストファイルの読み込み

テキストファイルをプログラムから読み込んで処理する場合、java.io.FileReader というクラスを使用すると便利である。

例えば、先ほど java.io.FileWriter で作成したファイル U:\hello.txt を読み込んで表示するプログラムを記述するには、次のように書けばよい。

package j2.lesson08.example;

import java.io.FileReader;
import java.io.IOException;

public class HelloReader {

    public static void main(String[] args) throws IOException {
        FileReader file = new FileReader("U:/hello.txt");
        try {
            // ファイルを一文字ずつ読み出すループ
            while (true) {
                // read()は -1 または 0~65535 の値が返ってくる
                int c = file.read();

                // 結果が -1 なら、ファイルを最後まで読み終わった
                if (c == -1) {
                    // return しても finally は実行される
                    return;
                }

                // 結果が -1 以外なら、char 型の文字を読み込んだ
                else {
                    // int -> char に変換
                    System.out.print((char) c);
                }
            }
        }
        finally {
            file.close();
        }
    }
}

このプログラムを実行すると、U:\hello.txt というファイルの中身である以下の内容がコンソールに表示される。

Hello, world!
こんにちは、世界!

最初のほうにある、

FileReader file = new FileReader("U:/hello.txt");

では、U:\hello.txt というファイルを読み込める状態にするためにファイルを開いている。この際、U:\hello.txt というファイルが存在しない場合は FileNotFoundException という例外が発生する。

少し飛ばして、

int c = file.read();

では、ファイルの中の一文字を読んでいる。手前の while (true) により、1回の繰り返しごとに先頭から順番に読み出されていく。順番に読んでいってファイルの最後に達してしまうと読み出すべき文字がなくなってしまうが、その際には -1 が返される。

if (c == -1) return;

読み出した値が -1 でなければ、その値は char 型に変換することによってファイル内の文字一文字になる。int 型の値は (char) というキャスト演算子によって char 型に変換できる。

else  System.out.print((char) c);

最後にファイルを閉じる。finally 節内に書かれているため、どんなことがあっても実行されるためファイルが閉じられることが保障される。

file.close();

FileWriter の場合と同様に、throws で呼び出し元に例外処理を任せることなく、このメソッド内で例外処理を行うこともできる。

package j2.lesson08.example;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class HelloReader2 {

    public static void main(String[] args) {
        FileReader file;
        try {
            file = new FileReader("U:/hello.txt");
        }
        catch (FileNotFoundException e) {
            System.out.println("ファイルが見つかりませんでした。" + e.getMessage());
            return;
        }
        try {
            // ファイルを一文字ずつ読み出すループ
            while (true) {
                // read()は -1 または 0~65535 の値が返ってくる
                int c = file.read();
                
                // 結果が -1 なら、ファイルを最後まで読み終わった
                if (c == -1) {
                    // return しても finally は実行される
                    return;
                }
                
                // 結果が -1 以外なら、char 型の文字を読み込んだ
                else {
                    // int -> char に変換
                    System.out.print((char) c);
                }
            }
        }
        catch (IOException e) {
            System.out.println("ファイル読み込みエラー。" + e.getMessage());
        }
        finally {
            try {
                file.close();
            }
            catch (IOException e) {
                // 何もしない
            }
        }
    }
}

java.io.FileReader での例外の扱い

java.io.FileReader は java.io.FileWriter と同様にファイルを扱うため、例外処理を多数行わなければならない。このクラスの各メソッドを呼び出した際に java.io.IOException が発生する可能性がある。

例外の階層

java.io.FileReader のコンストラクタは、throws java.io.FileNotFoundException が指定されている。

この FileNotFoundException は「extends IOException」の指定がされているため、java.io.IOException の子クラスである。そのため、FileNotFoundException は IOException として throws 指定したり catch したりすることができる

つまり、次のような場面で、

try {
    file = new FileReader("U:/hello.txt");
}
catch (FileNotFoundException e) {
    System.out.println("ファイルが見つかりませんでした。" + e.getMessage());
    return;
}

次のように java.io.IOException を捕捉してもよい。

try {
    file = new FileReader("U:/hello.txt");
}
catch (IOException e) {
    System.out.println("ファイルが見つかりませんでした。" + e.getMessage());
    return;
}

また、java.io.FileReader 全体では IOException と FileNotFoundException を発生させる可能性があるが、単に

public static void main(String[] args) throws IOException

とだけ書いても、FileNotFoundException が含まれることになる。

また、全ての例外クラスは java.lang.Exception というクラスの子クラスである。そのため、

public static void main(String[] args) throws Exception

とだけ書いてもIOException や FileNotFoundException, さらには NumberFormatException など全ての例外クラスが含まれることになる。

例外を階層化する場合に注意すべき点は、catch 節を複数書いた場合に上に書いた catch 節から順に捕捉できるか調べるということである。

try {
    int c = file.read();
    ...
    value = Integer.parseInt(input);
}
catch (Exception e) {
    // 例外処理すべて
}
catch (IOException ioe) {
    // 入出力の例外処理
}
catch (NumberFormatException nfe) {
    // 数値形式の例外処理
}

上記のようなプログラムを書いて、try 節で何らかの例外が発生すると、

  1. 最初の catch 節 - Exception で捕捉できるか調べる
  2. 2番目の catch 節 - IOException で捕捉できるか調べる
  3. 3番目の catch 節 - NumberFormatException で捕捉できるか調べる

という順序で例外の検査がされる。このとき、捕捉できる catch 節を一つでも見つけたら、その catch 節だけを実行する。つまり、try 節で IOException が発生しても catch (Exception e) の catch 節で IOException を捕捉できるため、「例外処理全て」と書いた部分だけが実行されて「入出力の例外処理」という部分は実行されない。NumberFormatException でも同様である。

チェックされない例外

IOException が発生する可能性のあるメソッドを呼び出した場合、呼び出した側は throws IOException をメソッドの宣言に追加するか、try-catch で捕捉しないとコンパイルエラーになってしまう。

しかし、Integer.parseInt(String) は NumberFormatException を発生させる可能性があるし、割り算は ArithmeticException を発生させる可能性があるにもかかわらず、throws や try-catch の指定をしなくてもコンパイルエラーにならない。これらは「実行時例外」と呼ばれる例外で、どれも java.lang.RuntimeException の子クラスである。

java.lang.RuntimeException を継承した例外クラスは、チェックされない例外クラスと呼ばれ、それ以外の java.lang.Exception を継承している例外をチェックされる例外と呼ばれる。チェックされる例外はコンパイルを行う際に throws や catch で適切に例外が考慮されているかどうかを調べ、例外が考慮されていない場合はエラーとなる。チェックされない例外はそのようなことを考える必要はない。

わかりにくい場合は、どんなメソッドを宣言しても暗黙のうちに throws RuntimeException が自動で追加されているものだと考えてもよい。つまり、RuntimeException の子クラスである例外は常に try-catch や throws をわざわざ書かなくてもコンパイルエラーにならない。

バッファ付きテキスト入出力

バッファつき入力として、java.io.BufferedReader というクラスがある。このクラスに java.io.Reader クラスを継承した FileReader などを与えると、readLine() という「テキストを一行読み出す」メソッドを使えるようになる。

このクラスは、これまでの講義で「コンソールから文字列を取得する」という目的で使ってきた。

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
..
String input = reader.readLine();

この、「new InputStreamReader(System.in)」にある InputStreamReader も java.io.Reader クラスを継承したクラスである。この部分をファイルを扱う Reader - java.io.FileReader - に変えることで、「コンソールから一行読む」ではなく「ファイルから一行読む」というプログラムを書けるようになる。

BufferedReader reader = new BufferedReader(new FileReader("U:/hello.txt"));
..
String input = reader.readLine();

一般に、ファイルへのアクセスはメモリへのアクセスよりも格段に遅い。BufferedReader はそのスピードの差を埋めるため、ファイルからデータを大量に読み出してメモリに保存しておき、read() メソッドが呼び出されたらメモリに保存されたデータを返す。ファイルから大量のデータを一度に転送する分にはそれなりのスピードが出るため、このようにメモリにたくさんのデータを先読みしておく技術はよく使われて「バッファリング」とよばれる。バッファとは一時的に蓄える場所の意味で、そのような場所を備えている Reader として BufferedReader が存在する。

BufferedReader の使い方は、コンストラクタ呼び出し時に引数として java.io.Reader のインスタンスを渡すだけである。java.io.FileReader は Reader の子クラスであるため、BufferedReader で扱うことができる (ここでもポリモーフィズムの考え方が使われている)。引数として java.io.Reader のインスタンスを渡すと、バッファ付き入力と引数に渡した Reader が接続され、バッファ付き入力に対して行った操作は接続先の Reader に対して行われるようになる。

BufferedReader reader = new BufferedReader(new FileReader("U:/hello.txt"));

よく使われるメソッドは readLine() というメソッドで、これは次の一行をコンストラクタに渡した Reader から読み出し String 型で返す。もし、読み出すべきデータがない (FileReader ならばファイルの終端まで読み終わった) 場合は、null という値が返される。

下記のように書くと、全ての行を順番に処理することができる。

while (true) {
    String line = reader.readLine();
    // null なら終了
    if (line == null) {
        break;
    }
    // line を処理する
}

他の書き方として、次のように書くこともある。

String line;
while ((line = reader.readLine()) != null) {
    // line を処理する
}

どちらも同じなので好きなほうを使えばよい。

また、読み出しが終了したら BufferedReader の close() メソッドを呼び出すことで、コンストラクタの引数に渡した Reader の close() が呼び出される。

// "U:\hello.txt" を開く
BufferedReader reader = new BufferedReader(new FileReader("U:/hello.txt"));
try {
    ...
}
finally {
    // "U:\hello.txt" を閉じる
    reader.close();
}

readLine() や、close() メソッドは、FileReader と同様に IOException が発生する可能性があるので注意すること。そのため、上記のプログラムでは throws IOException を指定するか、try 節で囲んで catch (IOException e) 節を用意する必要がある。

また、BufferedReader は Reader にバッファをつけたものであるが、Writer にバッファをつけた java.io.BufferedWriter というクラスも存在する。そちらも参照してみるとよい。