Javaプログラミング基礎 講義資料

最終回の主なテーマは、 ファイル、キーボードや画面への入出力です。 今までのプログラムは、 データをプログラムの中に決めうちで埋め込んでしまい、 計算結果を画面に表示するものでした。 ファイル名を指定し、ファイルからデータを読み込んだり、 データを書き込んだりする方法や、 キーボードからの入力を読み込む方法を説明します。

ファイルからの入力の例

ファイルの入出力を Java のプログラムで行うためには、 いくつかの約束ごとをマスターする必要があります。 ここでは、 例を用いてファイルの入力を行うプログラムの書き方を説明します。 対象はテキストファイルです。

ファイルへの入出力は、 Java のクラスライブラリにあらかじめ用意されている、 いくつかのクラスを利用することにより行います。

import java.io.*;

class DisplayFile {
    public static void main(String args[]) {
        BufferedReader in = null;
        try {
            in = new BufferedReader(new FileReader("sample.txt"));
        }
        catch (IOException e) {
            System.err.println("File cannot be opened.");
            System.exit(1); // ←終了コード 1 でプログラムを終了するメソッド
        }

        try {
            String str;
            str = in.readLine();
            while (str != null) {
                System.out.println(str);
                str = in.readLine();
            }
        }
        catch (IOException e) {
            System.err.println("An error was occured while reading.");
        }

        try {
            in.close();
        }
        catch (IOException e) {
            System.err.println("An error was occured while closing.");
        }
    }
}

このプログラムは、指定したテキストファイル (この例では sample.txt) の内容をそのまま画面に出力するものです。

新しいキーワード import と try 〜 catch があります。 これについて詳しくは後程説明します。 ここでは、プログラムがすべて順調に実行されると、 try の中カッコの中のみが実行されるとだけ考えてください。

ファイルからの入力の手順は次のとおりです。

まず、最初にファイルをオープンします。 オープンの際には、ファイル名を指定して、 ファイルの読み書きを行うことをシステムに準備させます。 システムは、その名前のファイルが存在するかどうかを調べるなど 必要な作業を行い、 ファイルの入出力を行う機能を持つオブジェクトを生成します。

例題のプログラムは次のようになっています。

in = new BufferedReader(new FileReader("sample.txt"));

new 演算子を用いてインスタンスを二重に生成しています。 これは、次のように書いてもほぼ同じ動きをします。

innerReader = new FileReader("sample.txt");
in = new BufferdReader(innerReader);

ここで得られた in は、 ファイル sample.txt の読みこみを行う機能を持ったオブジェクトです。 これ以降ファイルに対する読みこみ操作の際には、 in のメソッドを呼び出します。

ファイルがオープンされると、 実際にファイルからデータを読みこむことができるようになります。

次に、

str = in.readLine();

では、メソッド readLine を用い、 sample.txt から実際に 1 行ぶん読みこみ、変数 str に代入しています。 メソッド readLine を次々呼び出すことで、 ファイルの内容を先頭から順に読みこむことができます。 ファイルの終りに達すると readLine は null という値を返します。

ファイルに対するすべての読みこみが終わったら、 最後に close メソッドを実行します。 close によって in と実際のファイル (sample.txt) との結び付きが解除されます。

ストリーム

Java では、ファイル、キーボードや画面との入出力など、 プログラムの外部からデータが出入りするものをストリーム (stream) と呼ぶ概念で統一して扱います。

実際にデータの入出力を行う際には、 対象とするデータの種類やデータのある場所によって、 行うべき処理内容が異なってきます。 例えば、ローカルのディスクのファイルに対する入出力と、 キーボード/画面に対する入出力と、 ネットワークを介したサーバのデータ転送では、処理内容が異なります。

Java ではオブジェクト指向の特徴を生かし、 入出力データ全般に成り立つ性質をストリームと呼ばれるクラス群に持たせ、 それを継承する形で様々な種類のデータに対応するよう、 多くのクラスが用意されています。

先の例題の FileReader はファイルを読みこみ可能な状態にし、 実際にファイルから 1 文字ずつ読みこみを行う機能を持つストリームです。 FileReader は、 1 文字ずつファイルの内容を読み込むことしかできないので不便です。 そこで、行単位でファイルの読み込みを行う BufferedReader を用い、 FileReader のインスタンスにかぶせて使うことで 行単位の処理ができるようになります。

この「かぶせる」ことを英語で wrap と書くことから、 ラップすると呼ぶこともあります。 これは、以前説明した包含の考え方です。

ファイルへの出力の例

ファイルへの出力を行う場合、 クラス FileWriter, BufferedWriter, PrintWriter の 3 つのクラスを用います。

import java.io.*;

class WriteString {
    public static void main(String[] args) {
        PrintWriter out = null;

        try {
            out = new PrintWriter(new BufferedWriter(
                                  new FileWriter("sample.txt")));
        }
        catch (IOException e) {
            System.err.println("File cannot be opened.");
            System.exit(1);
        }

        out.println("Hello, Java.");
        out.println("You not only study Java,");
        out.println("but you also have to study data structures and algorithms");
        out.close();
    }
}

まず、

out = new PrintWriter(new BufferedWriter(
                      new FileWriter("sample.txt")));

については、次のように説明できます。

クラス FileWriter は 1 文字単位でファイルへ出力を行う基本的な機能を持つストリームです。 これを BufferedWriter によって行単位で出力を行えるように拡張 (ラップ) します。 さらに、 PrintWriter によって、 様々な型のデータをメソッド println を用いて出力することができるように 拡張します。

クラス PrintWriter のインスタンス out に対しては、 メソッド printlnprint を用いて データを出力することができます。 これらは、 今まで画面表示のために使ってきたメソッド System.out.printlnSystem.out.print と同じものです。

特にファイルへの書き込みを行う場合、 メソッド close でのファイルのクローズは忘れてはなりません。 必ずクローズしてください。 printlnprint の呼び出しによって書き込まれるデータは、 いったんバッファ (buffer) と呼ばれる一時的な記憶領域に保存され、 別のタイミングで実際にファイルに書き込まれます。 ファイルをクローズすることで、 バッファに残っているまだ書き込まれていないデータを 最後までファイルに書き込む処理が行われます。 クローズを忘れるとファイルの内容が中途半端になる可能性があります。

実は、これまで使ってきた System.out は、 システムに標準で備わっている画面に出力するためのストリームなのです。 System.out のストリームは特にインスタンスを生成しなくても、 使用することができるのです。

キーボード入力(1)

キーボードから入力を行うためのストリームに System.in があります。 次のプログラムは、 キーボードから入力された内容をそのまま画面に表示するプログラムです。

import java.io.*;

class KeyboardInput {
    public static void main(String[] args) {
        BufferedReader keyboard = new BufferedReader(
                                  new InputStreamReader(System.in));
        try {
            String str;
            str = keyboard.readLine();
            while (str != null) {
                System.out.println("Input Data: " + str);
                str = keyboard.readLine();
            }
        }
        catch (IOException e) {
            System.err.println("An error occured in key input");
        }
    }
}

このプログラムでは、 まず、キーボードからの入力ストリーム System.in を クラス InputStreamReader, BufferedReader で拡張 (ラップ) した インスタンスを生成しています。

次に、ここで生成したオブジェクト keyboard に対して、 メソッド readLine を用いることで行単位、 すなわち、 Enter または Return キーが押されるまでの入力を受け取り、 str に代入しています。 キーボードからの入力が終了する場合は、 Control+D (Control キーを押しながらアルファベットの D のキー) を押します。 プログラムの中では、入力は終了したものとして readLine の値が null となります。

キーボード入力(2)

キーボードからの入力を行うもう一つの方法を紹介します。

次のプログラムは、キーボードから入力された内容を int型の整数として受け取り、表示するプログラムです。 キーボードからの入力を受け付けるという機能は同じですが、 別のクラスを用いて行う方法です。

import java.util.*;

class KeybordInput2 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // ← (A)

        System.out.println("整数を3つ入力してください: ");

        int a = scanner.nextInt();                // ← (B)
        int b = scanner.nextInt();
        int c = scanner.nextInt();
        int d = a + b + c;
        System.out.println(a + "+" + b + "+" + c + " = " + d);
    }
}

まず、プログラムの(A)の行のように、キーボードからの入力を行う前に、 ストリーム System.in を指定しクラス Scanner の オブジェクトを用意しておきます。

次に、キーボードからint型の整数値の入力を受け付けたいところで、 プログラムの(B)の行のように、メソッド nextInt() を呼び出します。 このプログラムでは、3つの整数を入力するために、 nextIntを3回たて続けに呼び出しています。 このメソッドからの戻り値が、キーボードから読み込んだ値となります。

入力する値は、int型で表現できる範囲 (約-21億〜約21億) に収まっている必要があります。 また、整数以外のものを入力してはいけません。

上の例題では、int型の整数値を入力する方法を説明しましたが、 読み込みを行うメソッドを使い分けることで、 さまざまな型のデータを読み込むことができます。 主な、読み込む型と、呼び出すメソッドの対応を以下に示します。

nextInt()
int型整数を読み込む。戻り値の型はint。
nextDouble()
double型浮動小数点数を読み込む。戻り値の型はdouble。
next()
文字列を読み込む。ただしスペースや改行などは文字列の区切りとみなされ、区切られた別々の文字列として読み込まれる。戻り値の型はString。
nextLine()
1行分の文字列を読み込む。戻り値の型はString。
hasNext()
入力の終わりを検出する。入力の終わりに達していた場合は false、 それ以外の場合は true を返す。戻り値の型はboolean

標準入力、標準出力、標準エラー出力

System.out, System.in を用いて入出力を行うことを行ってきましたが、 このときの入出力の相手を標準出力および標準入力と呼びます。 特に指定がなければ、標準出力はディスプレイ画面、 標準入力はキーボードに結び付けられます。

Linux をはじめとする UNIX では、プログラムを起動する際に、 標準入力や標準出力を特定のファイルに結び付けるように指定することができます。 このようなファイルの切り替えをリダイレクトと呼びます。

例えば、

$ java AClass  >  ファイル名

とすると、プログラムの出力が指定されたファイルに書き込まれます。 普通なら画面に出力されていた内容が出なくなり、 同じ内容がファイルに記録されます。 これは、プログラムの出力結果をファイルに残したい場合などに便利です。 リダイレクトは Java に限らず UNIX のコマンドであれば、ほとんどのものに同じように適用できます。

逆に、

$ java AClass < ファイル名

とすると、キーボードの代わりに入力データをファイルから読み込みます。 プログラムに処理させたいデータが大量にある場合、 キーボードからの入力は大変なので、 あらかじめエディタや別のプログラムでデータを用意してから、 この方法で入力させることができます。

さらに、

$ java AClass < 入力ファイル名 > 出力ファイル名

のように、入力と出力の両方を別々のファイルに結び付けることもできます。

ところで、もう一つ標準エラー出力と呼ばれるものがあります。 標準エラー出力は、もっぱらエラーメッセージを出力するために使われます。 標準エラー出力のストリームは System.err です。 例題のプログラムでは、エラーメッセージの出力に System.out ではなく System.err を使っています。

特に指定がなければ、標準エラー出力は画面上に出力されます。 標準出力と別のものを用意してあるのは、 リダイレクトを行った場合でもエラーメッセージは画面に表示させたいからです。 上の例のように標準出力をリダイレクトしていても、 標準エラー出力は画面上に表示されるので、 エラーメッセージを見逃してしまうことはありません。

バイナリファイルの扱い

これまで説明したストリームの使用方法は、 主にテキストファイルを対象とした方法です。 画像などのバイナリデータを取り扱うにはクラス InputStream/OutputStream を用いる必要があります。 詳細は本講義では取り上げません。

例外処理

プログラムの実行中に、 望まれた動作ができない状況が発生することがあります。 例えば、ファイルを読みこもうをしたときに指定されたファイルが存在しない場合や、 ファイルが書き込み禁止になっていたりした場合です。 このような、プログラムの実行中に起きる問題を例外 (exception) と呼びます。

ファイルが正しく書き込めなかったのを無視して作業を進めれば、 せっかくの処理結果を失ってしまい、これでは大変なことになります。 例外が発生したとき、 その問題を正しく処理する必要があります。

try 〜 catch 文

ファイルのオープンや、読み込み、書き込みなど、 例外が発生する可能性のある処理を try の中カッコの中に記述します。 try の中の処理で例外が発生した場合、 catch の中の処理が行われます。 ここには、エラーメッセージを表示したり、 別の処理で代替するなどの、 例外の原因となった問題を解決するための処理を書きます。

try 〜 catch 文の文法的な説明は以下のとおりです。

try {
    ....
    例外が発生する可能性のある処理
    ....
}
catch (例外の型  変数名) {
    「例外の型」と同じ例外が発生した際のエラー処理
    ....
}
catch (例外の型  変数名) {
    「例外の型」と同じ例外が発生した際のエラー処理
    ....
}
...
...
finally {
    例外の発生の有無に関わらず行われる処理
    ....
}

実は、例外もクラスとして表されます。 例外の種類によって、さまざまな例外のクラスがあります。 例えば、ストリームを用いて入出力を行っている際に よく発生する例外は IOException です。

例えば、例外 IOException が発生したときの例外処理は次のようになります。

try {
    ...
    ...
}
catch (IOException e) {
    例外処理の内容
    ...
}

変数 e は例外のオブジェクトです。 例外のオブジェクトは、 例外の起こった場所、直接の原因など、 例外処理に役立つ様々な情報を持っています。

例外処理についての詳細は、教科書を参照してください。

import 文

Java のクラスライブラリには様々なクラスが用意されていますが、 これらは階層的に管理されています。 例えば、 FileReader は正確には java.io.FileReader というクラスです。 これは、java.io というパッケージに属する FileReader というクラス、 という意味です。

クラス java.io.FileReader を使用するには、単に

in = new java.io.FileReader("sample.txt");

のようにクラス名の正確に書けば良いのですが、クラス名が長くなり面倒です。 プログラムの冒頭で import 文を書くことにより java.io を省略することができます。

import java.io.FileReader;
...
...
    in = new FileReader("sample.txt");
...

さらに、 import java.io.*; のようにアステリスク (「*」) を用いると、 パッケージ java.io に属するすべてのクラスが import されます。

ただし、 java.lang パッケージに属するクラスは、 import 文を使用しなくて使用できます。 java.lang に属するクラスの例には、 String や System などがあります。

パッケージについての詳細は、 教科書を参照してください。

Java のクラスライブラリの パッケージとクラスの構成については、 JDK 5.0 ドキュメント、 もしくは、 /usr/java 以下に展開した同様のドキュメントを参照してください。

インスタンスメンバと静的メンバ

今回のもう一つの話題はメンバの種類についてです。

オブジェクトの属性とメソッドをメンバと呼びます。 メンバにはインスタンスメンバと静的メンバの二種類があります。

これまでのプログラムで用いてきたクラスの属性やメソッドは、 インスタンスを生成することで使用することができます。 これらのメンバをインスタンスメンバと呼びます。 一方、インスタンスを作成せずに使用できる属性やメソッドもあります。 これを静的メンバと呼びます。

インスタンスメンバ

これまで、用いてきた属性やメソッドは、 メソッド main を除き、 全てインスタンスメンバに分類されます。

インスタンスは、クラスをもとに生成される「実体」のことでした。 インスタンスはいくつでも生成することができ、 個々のインスタンスはそれぞれが別個のものとして振る舞います。 また、その属性もインスタンスごとに別個の値を保持します。

このように個々のインスタンスが独立してもつ属性やメソッドのことを インスタンスメンバ (instance member) と呼びます。

クラスをもとにインスタンスを一つ生成すれば、 インスタンスメンバも各一つ存在することになり、 インスタンスを n 個生成すれば、 インスタンスメンバも n 個存在することになります。

インスタンスメンバは個々のインスタンスで独立したメンバであり、 複数のインスタンスで共有することはできません。 また、インスタンスメンバにアクセスするには、 あらかじめ、インスタンスを生成しておく必要があります。

静的メンバ

静的メンバはクラスに属する属性やメソッドです。 オブジェクトの有無とは無関係に使うことができます。 したがって、静的メンバを使うためにはインスタンスを生成する必要はありません。

静的属性

静的属性 (static field, 静的変数) は、インスタンスごとに独立した値とはならず、 クラス単位で共通に使える属性です。 静的属性はクラスに対して一つだけ割り当てられる変数であり、 同じクラスを型とする全てのインスタンスで共有されます。 また、静的属性の値はオブジェクトの生成、消滅とは無関係に、 プログラムの実行を開始してから終了するまで値が保持されます。

静的属性を宣言するには、 次のように、クラス定義の中の属性の宣言部分にキーワード static を用います。

static 型名 変数名;

静的属性の使用例として定数があげられます。 定数はクラスに共通して使用される値であり、 途中で変更することがありません。 そこで、定数であることを示すキーワード final と、 アクセス修飾子 public を用いて定数を宣言すると以下のようになります。

class MathConstants {
    public static final double PI = 3.14;
    public static final double E = 2.72;
}

class Circle {
    ....
    // 上で定義した定数を用いた計算例
    area = radius * radius * MathConstants.PI;
}

静的属性にアクセスするには、 クラス名と属性名をドットでつなぎ、以下のように記述します。

クラス名 . 属性名

なお、 同じクラス定義の中では「クラス名 . 」の部分を省略することができます。

静的メソッド

静的メソッド (static method) は、 インスタンスの有無に関わらず利用することができるメソッドです。 静的メソッドを宣言するには、キーワード static を用いて以下のように書きます。

static 返り値の型 メソッド名 (引数1, 引数2, ...)

静的メソッドを呼び出すには、 クラス名とメソッド名をドットでつなぎ、以下のように記述します。

クラス名 . メソッド名 ( 引数1, 引数2, ... )

静的メンバの利用例

静的メンバを使う場面は限られています。 例として、 クラスに関係する定数を静的属性として宣言する場合や、 オブジェクトを生成する必要がない処理のまとまりを 静的メソッドとして宣言すること等が考えられます。

次のプログラムは、四則演算を行うクラス Caluculation の例です (0 で除算した場合の処理は省略)。

class Calculation {
    public static int add(int a, int b) {
        return a + b;
    }
    public static int subtract(int a, int b) {
        return a - b;
    }
    public static int multiply(int a, int b) {
        return a * b;
    }
    public static int divide(int a, int b) {
        return a / b;
    }
}

四則演算の機能を実現するメソッドを考えると、 インスタンスを生成する必要がありません。 このような場合は、静的メソッドを用いるのが適していると言えます。 しかし、計算途中の値を覚える等の機能を持つ等、 内部の状態を属性として持つようなクラスは、 静的メソッドを用いず、インスタンスを生成して使うべきです。

一般に、プログラムで行う仕事の内容を考えたときに、 対象 (もの) として考えられるものについては、 インスタンスを生成すべきだと言えます。

メソッド main

プログラムの実行が開始されるメソッドとして main メソッドがあります。 これは、 public 、すなわち外部から実行可能なメソッドとして、 以下のように宣言されています。

public static void main(String[] args) { ... }

実行時には、 java コマンドで指定されたクラス内にあるメソッド main が、 まず最初に実行されるという決まりになっています。 また、メソッド main は static メソッドとして宣言されていますので、 インスタンスを生成することなく実行できます。