Powered by SmartDoc

Java言語(文字ストリーム)

担当: 佐々木重雄

ストリームとは

先頭から順にアクセスする,均一なデータの並びをストリームという。ストリームは,主に,ファイルや入出力装置へのアクセスの出入り口として用いられる。オペレーティングシステム内部では,入出力に関して複雑な処理が行なわれるのであるが,あくまでも内部の話であり,ストリームを利用するプログラムは,極めて簡潔になる。

ストリームの概念は,Unixオペレーティングシステムで示され,現在,主流の考え方になっている。Javaでも,入出力はストリームを通して行なう(1)。Javaのストリームは、そこを流れるデータの型によって、バイトストリーム、文字ストリーム、データストリーム、オブジェクトストリームの4つに分類される。これらには、それぞれ、バイト型、文字型、基本型(整数や浮動小数点数)、オブジェクト(クラスのインスタンス)のデータが流れる。なお文字ストリームは、国際化に対応するためJDK 1.1から導入された。この資料では、主に文字ストリームを取り上げる(必要最小限バイトストリームも取り上げる)。データストリーム、オブジェクトストリームは取り上げない。

  1. JDK 1.4で導入されたNew I/O (java.nio)により,メモリパップI/Oが可能になったが,それ以前はストリーム以外の手段で入出力を行なうことができなかった。

java.ioパッケージを概観する

Javaの入出力に関わるクラス群は、原則として、パッケージjava.ioにまとめられている。java.ioの主なクラスを、次の表に示す。

パッケージ入力クラス出力クラス
java.io InputStream OutputStream
Reader Writer
InputStreamReader OutputStreamWriter
BufferedReader BufferedWriter
PrintWriter
PrintStream
StreamTokenizer
File
FileInputStream FileOutputStream
FileReader FileWriter

Javaプログラムで、画面への出力に使うSystem.out.println()の頭に付いているSystem.outはPrintStreamクラスのインスタンスである。System.outの逆のSystem.inというオブジェクトもあり、こちらはInputStreamクラスのインスタンスである。PrintStreamは「至れり尽くせり」という感じの便利なクラスだが、InputStreamは、原始的で、利用方法も煩雑である。入力プログラムを作成するためには、この資料の内容を一通り理解する必要がある。

文字ストリームを使うには

文字ストリームは、バイトストリームの上に被せる(wrapする)形で生成し、プログラム中では、(バイトストリームを直接使うのではなく)文字ストリームを利用する。InputStreamReaderとOutputStreamWriterは、(フィルタ型)文字ストリームのクラスである。この2つのクラスのコンストラクタは、バイトストリームから(それをラッピングする)文字ストリームを生成する。

まず簡単な例として、System.in, System.outをベースに文字ストリームを作る。プログラム断片を次に示す。

import java.io.*;
...
  public static void main(String[] args)
     throws IOException
  {
    InputStreamReader  in  = new InputStreamReader(System.in);
    OutputStreamWriter out = new OutputStreamWriter(System.out);

    int c = in.read();
    out.write((char)c);

クラスReaderにはread()メソッドがあり、クラスWriterにはwrite()メソッドがある。read()メソッドは文字型(char型)のデータを返すことを目的とする。ところがマニュアルを読むと、read()メソッドの戻り値はint型である。これは、ファイルの末尾に達したり、途中で何らかのエラーが発生してデータが読めなくなったときに、char型の範囲をはみ出した値(具体的には-1)を返すためにint型になっている。int型のデータをchar型に変更するためには、キャスト(cast)という操作を行なう。変数cをchar型にキャストするには(char)cと記載する。

mainの次の行のthrows IOExceptionというのは、メソッドを実行中に、例外IOExceptionが発生したら、そのまま丸投げすることを宣言している。

Javaでは、入出力に関わるほとんどのメソッドについて、「例外」が伴うことが宣言されており、入出力を利用するプログラミングの際、「例外」の対処方法を記述しなければならない。ここで「例外」とは、エラーを一般化した概念で、通常でない処理を必要とする事態が発生したことを告知する仕組みである。「例外」が発生すると、通常、Javaのプログラムはエラーメッセージを表示して停止する。

近代的なプログラミング言語は「例外」発生時の対処方法をプログラム中に記述することが可能である。Java言語では、更に一歩押し進めて、例外発生の可能性があると宣言されたメソッドを利用する際は、例外の対処方法を明示的に記述するか、例外を丸投げすることを宣言することを要請している。この授業では、原則的に、例外は丸投げすることにする。

参考までに、Java言語を含め、例外を明示的に扱える言語では、例外を意図的に発生させることができ、これを、少々くだけた言い回しで「例外を投げる」(throw an exception)という。逆に「例外を受ける」(catch an exception)とは、例外処理をすることである。例外は、メソッド呼出しの下請け側(子側)から、呼出し元(親側)に向かって投げられる。「例外の丸投げ」というのはこの資料だけの言い回しだが、子から投げられた例外を、何も加工せず、親に投げることのつもりである。

ところでmainメソッドの親とは何だろうか。これはJavaの実行系(javaコマンド)そのものである。mainメソッドから例外が投げられると、Javaの実行系は、エラーメッセージを表示して停止する。

日本語を使うには

InputStreamReaderやOutputStreamWriterは、特に指定しなければ、下位のストリームのバイトデータをUnicodeと解釈して処理する。しかしInputStreamReaderとOutputStreamWriterは、第2引数に文字符号化名を与えることにより、その地域の文字コードを扱うことができる。文字符号化名として、``JIS'', ``EUCJIS'', ``SJIS''などを使うことができる。たとえば次のようにする。

InputStreamReader  in  = new InputStreamReader(System.in, "JIS");
OutputStreamWriter out = new OutputStreamWriter(System.out, "JIS");

int c = in.read();
out.write((char)c);

バッファリングによる高速化

InputStreamReaderやOutputStreamWriterは、readやwriteが実行されるたびに実際に入出力の処理を行なうので、実行効率が悪い。バッファリング(2)によって実際に入出力をまとめて行なえば、効率が良い。次のようにする。

InputStreamReader r = new InputStreamReader(System.in, "JIS");
BufferedReader   in = new BufferedReader(r);
OutputStreamWriter w = new OutputStreamWriter(System.out, "JIS");
BufferedWriter   out = new BufferedWriter(w);

String str = in.readLine();
out.write(str);

BufferedReaderにはreadLine()というメソッドがあり、1行のデータを文字列として得ることができる。文字列は、そのままWriterオブジェクトを通じてwriteできる。

  1. buffer:緩衝器。入出力を効率的に行なうためのデータ構造で、生産者が生成したデータを消費者が利用するまで蓄えておく場所をバッファといい、それを使うことをバッファリングという。生産と消費のタイミングが合わない場合、バッファリングを行なうことで待ち時間が無くなり、実行効率を向上させることができる。入出力の処理は、データがある程度の大きさにまとまってから行なった方が効率よいので、バッファリングの効果は大きい。バッファリングの効率の良さは、コップ一杯の水を飲みたいが、水は井戸から「つるべ」を使って汲上げることしかできない状況を考えればわかりやすいだろう。井戸からコップ一杯の水を飲むたびに井戸から汲上げるより、汲んだ水を桶に溜めておき、そこから飲みたいだけの水を飲んだ方がよい。

print と println

OutputStreamWriterやBufferedWriterは、printやprintlnというメソッドを持たないが、PrintStreamあるいはPrintWriterを重ねてやると使えるようになる。

OutputStreamWriter w = new OutputStreamWriter(System.out, "JIS");
BufferedWriter    bw = new BufferedWriter(w);
PrintWriter      out = new PrintWriter(bw, true);

out.print("3 times 4 makes ");
out.println(3 * 4);

new PrintWriterの第2引数のtrueは、printlnを実行したら、自動的にバッファの掃き出しをするという指定である。これをfalseにするか省略すると、バッファが満杯になるまでデータは掃き出されない。この動作は、対話的な処理を実行するときに困る。printlnを実行して画面には何も表示されない。対話処理をするときはtrueの指定をするべきである(3)

対話処理を行なっていて、プロンプトを表示するなど、行の途中で掃き出しを行ないたいときはout.flush();を実行する。

  1. ファイル出力の場合、改行ごとに掃き出す必要はなく、バッファが満杯になってから行なえばよい。実行性能の観点からは、掃き出し回数は少ない方がよいので、ファイル出力の時は自動掃き出しの指定はしない。

プログラム例

標準入力(コンソール)で入力を受け付け、小文字を大文字に変換して表示するプログラムを次に示す。

import java.io.*;

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

        String line;
        while ((line = r.readLine()) != null) {
            System.out.println(line.toUpperCase());
        }
    }
}

ファイル入出力

ファイルからの入出力を行なうには、次の手順をとる。

  1. Fileのコンストラクタで、ファイル名からファイル・ハンドラを得る。
  2. FileInputStream, FileOutputStreamのコンストラクタで、ファイル・ハンドラからバイトストリームを作る。
  3. バイトストリームから文字ストリームを作る。
  4. 文字ストリームからバッファ付き文字ストリームを作る。
  5. 必要に応じてPrintWriterを使う。

Fileはファイル・ハンドラ、FileInputStreamはFileから入力を行なうバイトストリーム、FileOutputStreamはFileへ出力を行なうバイトストリームである。Fileのコンストラクタでファイルを指定し、FileInputStream, FileOutputStreamでそのファイルを開くことができる。FileInputStream, FileOutputStreamはバイトストリームなので、InputStreamReader, OutputStreamWriterを使って、文字ストリームに格上げする。

例として、ファイルをコピーするプログラムをlist[FileCopy1.java]に示す。

FileCopy1.java
import java.io.*;

public class FileCopy1 {
    public static void main(String[] args)
	throws IOException
    {
	File infile = new File("input");
	FileInputStream is = new FileInputStream(infile);
	InputStreamReader isr = new InputStreamReader(is);
	BufferedReader br = new BufferedReader(isr);

	File outfile = new File("output");
	FileOutputStream os = new FileOutputStream(outfile);
	OutputStreamWriter osw = new OutputStreamWriter(os);
	BufferedWriter bw = new BufferedWriter(osw);
	PrintWriter pw = new PrintWriter(bw);

	String line;

	while ((line = br.readLine()) != null) {
	    pw.println(line);
	}
	pw.close();
    }
}

List[FileCopy1.java]は,わざと律儀に書いたもので,実際にはもう少し簡潔に記述できる。FileReader, FileWriterを使うと、ファイルの指定から文字ストリームを作るところまで自動的に行なってくれる。そのコード例をlist[FileCopy2.java]に示す。

FileCopy2.java
import java.io.*;

public class FileCopy2 {
    public static void main(String[] args)
	throws IOException
    {
	FileReader fr = new FileReader("input");
	BufferedReader br = new BufferedReader(fr);

	FileWriter fw = new FileWriter("output");
	BufferedWriter bw = new BufferedWriter(fw);
	PrintWriter pw = new PrintWriter(bw);

	String line;

	while ((line = br.readLine()) != null) {
	    pw.println(line);
	}
	pw.close();
    }
}

ただし現行バージョンのFileReader, FileWriterでは、文字コードの指定ができない。それを必要とするときは、InputStreamReader, OutputStreamWriterを併用(プログラム例はlist[FileCopy3.java])する必要がある。

FileCopy3.java
import java.io.*;

public class FileCopy3 {
    public static void main(String[] args)
	throws IOException
    {
	FileInputStream is = new FileInputStream("input");
	InputStreamReader isr = new InputStreamReader(is, "SJIS");
	BufferedReader br = new BufferedReader(isr);

	FileOutputStream os = new FileOutputStream("output");
	OutputStreamWriter osw = new OutputStreamWriter(os, "JIS");
	BufferedWriter bw = new BufferedWriter(osw);
	PrintWriter pw = new PrintWriter(bw);

	String line;

	while ((line = br.readLine()) != null) {
	    pw.println(line);
	}
	pw.close();
    }
}

WWWページの入力

ファイルだけでなく,TCP/IPの通信もストリーム入出力の対象である。TCPは,パケット通信機能のみを提供するIPの上の階層のプロトコルで,パケットの複雑なやり取りを隠蔽し,バイト単位のデータを順序正しく送受信する機能を提供する。そのためTCPとストリームの相性は良く,先頭から順にアクセスする処理なら,あたかもファイル入出力をするかのようにTCPの通信プログラムを書くことができる。

この授業は,ネットワークプログラミングを目的としないので,ここではごく簡単に,ウェブサーバに置かれたファイルを読むプログラムだけ示す。java.net.URL(java.netパッケージのURLクラス)により、WWWアクセスができる。list[WebRead.java]はその使用例である。

簡単に説明すると,クラスURLは,URL (Uniformed Resource Locator)の情報を表わすもので,そのインスタンスは,通信プロトコル,サーバー(のドメイン名またはIPアドレス),ファイルの置場所(パス名)の情報を保持している。URLのインスタンスに対し,openStream()メソッドを実行することで,サーバーに置かれたファイルにアクセスするためのバイトストリームを作ることができる。

WebRead.java
import java.io.*;
import java.net.*;

public class WebRead {
    public static void main(String[] args)
	throws IOException
    {
	URL u = new URL("http://www.is.akita-u.ac.jp/");
	InputStream is = u.openStream();
	InputStreamReader isr = new InputStreamReader(is, "JISAutoDetect");
	BufferedReader r = new BufferedReader(isr);
	String line;

	while ((line = r.readLine()) != null) {
	    System.out.println(line);
	}
    }
}

	

秋田大学では,ファイアウォールによって,プロクシサーバーを経由しないHTTPの通信を遮断しているので,list[WebRead.java]のプログラムを変更して学外のサーバーのURLを指定しても,アクセスに失敗する。学外のページにアクセスするためには,プロクシーサーバーを経由するようにしなければならない。

マニュアルを参照すると,クラスURLのコンストラクタは何種類かあり,次の2つの文は同じ意味である(80は,HTTPに割当てられたポート番号である)。

URL u = new URL("http://www.is.akita-u.ac.jp/index.html");
URL u = new URL("http", "www.is.akita-u.ac.jp", 80, "/index.html");

さて,プロクシーサーバーとは何かというと,クライアントの代理(proxy)となって,サーバーにアクセスしてファイルを取得し,それをクライアントに返す,中間サーバーである。上の例の2番目のnew URL(...)において,第2引数,第3引数に,それぞれ,プロクシーサーバーの名前とポート番号,第4引数にアクセスしたいURLの文字列を与えることで,プロクシーサーバーを経由してアクセス可能である。

秋田大学ではwww.is.akita-u.ac.jp,ポート番号8080でプロクシーの機能を提供しているので,次のようにすれば,東北大学のトップページにアクセスできる。

URL u = new URL("http", "wproxy.is.akita-u.ac.jp", 8080,
                "http://www.tohoku.ac.jp/");

StreamTokenizer

java.ioパッケージに含まれるStreamTokenizerクラスは、簡単な字句解析機能を提供する。本格的な字句解析をするには、専用のツールを使うか、コンパイラの教科書にしたがって、プログラムを0から記述しなければならないが、電卓程度のプログラムであれば、StreamTokenizerを使えば充分である。マニュアルは、次のURLから参照できる。

http://www.is.akita-u.ac.jp/local/java/jdk1.3/docs/ja/api/java/io/StreamTokenizer.html

StreamTokenizerは、単純な機能しか提供しないとはいえ、一人前の字句解析ツールであるから、その機能を理解していないと、使い方すら理解できないだろう。しかし、具体的な使用例を見れば、だいたいの使い方は理解できるはずである。

LexTest.java
import java.io.*;

public class LexTest {
    public static void main(String[] args)
	throws IOException
    {
	InputStreamReader ir = new InputStreamReader(System.in);
	BufferedReader r = new BufferedReader(ir);
	StreamTokenizer st = new StreamTokenizer(r);

	st.ordinaryChar('/');
	st.slashStarComments(true);
	st.slashSlashComments(true);

	while (st.nextToken() != StreamTokenizer.TT_EOF) {
	    switch (st.ttype) {
	    case StreamTokenizer.TT_EOL:
		System.out.println("newline");
		break;
	    case StreamTokenizer.TT_NUMBER:
		System.out.println("number: " + st.nval);
		break;
	    case StreamTokenizer.TT_WORD:
		System.out.println("word: " + st.sval);
		break;
	    case '"':
	    case '\'':
		System.out.println("string: " + st.sval);
		break;
	    default:
		System.out.println("punctuation: " + (char)st.ttype);
	    }
	}
    }
}

StreamTokenizer.TT_EOFなどの定数名が長くて、入力が少し大変であるが、これが正書法である。JDK 1.5からは,プログラムの先頭で

import static java.io.StreamTokenizer.TT_EOF;
import static java.io.StreamTokenizer.TT_NUMBER;
...

と書くと,単にTT_EOF, TT_NUMBERと書くことができる。

プログラムの実行は、Javaコンソールを開いて1 + 2 * 3のような文字列を入力する。


JDK 1.1 でサポートする文字符号系

Sunの資料を一部引用すると,文字符号系に示す文字コードを扱うことができる。

文字符号系
文字符号名 通称
EUCJIS Japanese EUC
JIS JIS
KSC5601 KSC5601 Korean
SJIS PC and Windows Japanese
UTF8 Standard UTF-8
8859_1 ISO Latin-1
8859_2 ISO Latin-2
8859_3 ISO Latin-3
8859_4 ISO Latin-4
8859_5 ISO Latin/Cyrillic
8859_6 ISO Latin/Arabic
8859_7 ISO Latin/Greek
8859_8 ISO Latin/Hebrew
8859_9 ISO Latin-5

JDK 1.2以降では、扱える文字符号系が大幅に増加されたほか、文字符号系の名称が若干変更になっている。

http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html

を参照のこと。学内からの閲覧に限定されるが、

http://www.is.akita-u.ac.jp/local/java/jdk1.3/docs/ja/guide/intl/encoding.doc.html

で日本語訳を参照できる。

オンライン・リソース

この文書は次のURLから参照できる。http://www.is.akita-u.ac.jp/~sig/lecture/java/char-stream.html