Revised: Feb./23rd/2003
コンピュータは、入力されたデータを処理して出力するものです。データ入出力(I/O: Input/Output)は、そのデータ処理のロジック実装と同じく、重要な機能です。Javaでは、データI/Oはjava.ioパッケージ内のクラスとして実装され、これらのオブジェクトに対する入出力として実現します。Javaでは、目的毎に多数のクラスが定義されており、パフォーマンス、動作特性が異なります。
ストリームついては、詳しく説明済みですが、ここではタスク指向の Tips としてまとめなおします。
例えば、FileInputStreamのコンストラクタは三つあり、それぞれ引数として、パスを表す文字列、Fileオブジェクト、FileDescriptorオブジェクトをとります。また、ファイルに特化したストリームであるRandomAccessFileというクラスもあります。
ストリームでファイルにアクセスするには、ネイティブのファイル・システムに対するAPIを実装する必要があります。しかし、ネイティブ・ファイル・システムに依存する内容をハード・コーディングすると、中間言語たるJavaの可搬性という特徴が失われてしまいます。つまり、Windowsでコンパイルしたバイト・コードがUNIX/Linux上で実行できなくなってしまうのです。
この問題を解決するために、Javaではファイル・アクセスをプラットフォーム独立になるように抽象化するクラスを用意しています。
ストリームの枠組みではFileクラス型オブジェクトを使って、ネイティブのファイル・システムを抽象化して可搬性を確保します。ストリームの枠組みから外れて、ランダムアクセスを利用する場合はRandomAccessFileを利用します。FileDescriptorは殆ど使われません。
ストリームを単独で使うことは殆どなく、他のストリームのコンストラクタに引数として与えます。
オブジェクトを他のオブジェクトにくるんでしまうことをラッピングと呼びます。ストリームも、ラッピングによって、要件を果たす適切な機能を実装することができます。
例えば、ファイルからのテキスト入力の場合、FileReaderのread(), readLine()で読み込みますが、これらは読み出し毎にファイルアクセス、バイトの文字列変換、クローズを繰り返すので著しく効率が悪くなります。ディスクは細切れのレコードを繰り返し読むよりも、一定サイズのブロックを小数回読み込む方が効率的です。
この問題を回避するために、バッファを持ったストリームでラップします。BufferedReaderの場合、コンストラクタはReader型オブジェクトを引数にとるので、Readerクラスのサブクラス型の任意のストリームを引数に持つことができます。
// FileReaderストリームの作成 FileReader fin = new FileReader("myText.txt"); // BufferedReaderでfinをラップ BufferedReader bin = new BufferedReader(fin);
これは通常、次のように書きます。
Reader bin = new BufferedReader( new FileReader("myText.txt"));
ここではReader型ストリームを例にとりましたが、ラッピングやバッファリングの特徴は、バイト・ストリーム、文字ストリーム供に共通です。次の例は、FileInputStreamをProgressInputStreamでラップし、更にBufferedInputStreamでラップしています。
InputStream bin = new BufferedInputStream( new ProgressMonitorInputStream(myFrame, "読み込み中...", new FileInputStream("myData.dat")));
ストリームは他のストリームでラップすることで機能拡張できます。バッファを持つストリームでラップする形を憶えてください。
全てのストリームは、最初はバイト・ストリームです。InputStreamReader/OutputStreamWriterで文字ストリームに変換します。
リスト8は、ネットワークのSocketから入出力ストリームを取得し、文字ストリームに変換しています。尚、ソケットから入出力ストリームを取得するために、Socketクラスに定義されている、ストリームを生成するファクトリ・メソッドgetXxxStream()を使っています。このように、ストリームをファクトリ・メソッドで生成する実装は、Java 2コア・パッケージのいたるところで見られます。
▼リスト8
// "www.idg.co.jp/jw/"のポート80へのソケット Socket socket = new Socket("http://www.idg.co.jp/jw/", 80); // ソケットから入出力ストリームの取得 InputStream sin = socket.getInputStream(); OutputStream oin = socket.getOutputStream(); // 文字ストリームへ変換 Reader rin = new InputStreamReader(sin); Writer wout = new OutputStreamWriter(sout); // バッファリング BufferedReader in = new BufferedReader(rin); BufferedWriter out = new BufferedWriter(wout);
リスト8で見たとおり、バイト・ストリームを文字ストリームに変換するには、InputStreamReader/OutputStreamWriterを使います。例えば、これらのサブクラスである、FileInputReader/FileOutputWriterは、InputStreamReader/OutputStreamWriterの引数にFileInputStream/FileOutputStreamを与えたものに過ぎません。
文字コードを指定するには、InputStreamReadrとOutputStreamWriterを使います。
実は、InputStreamReaderとOutputStreamWriterの、引数が一つだけのコンストラクタでも、暗黙的にシステムのデフォルトの文字コードが使われていたのです。この場合は、Javaの内部コードであるUNICODEと、システムのデフォルト文字コード間で、自動的に変換されます。
文字コードを明示的に指定するために、次のようなコンストラクタも定義されています。
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException
それぞれ、第二引数に文字コードを指定します。J2SE 1.4では、java.nio.charsetパッケージに、文字セットを表すCharsetクラスも使えます。サポートされている文字コードのリストは、次のURLから得られます。
日本語が使える代表的な文字コードを、表3に挙げておきます。基本的には、IANAに登録された名前で指定することが推奨されます。
文字符号化方法 | 説明 |
---|---|
UTF-8 | 8ビットUCS変換形式 |
UTF-16 | 16ビットUCS変換形式 |
Shift_JIS (SJIS) | シフトJIS |
Windows-31J (MS932) | Windows用日本語文字コード ※1.4.2ではShift_JISと非互換 |
ISO-2022-JP (ISO2022JP) | JIS |
EUC-JP (EUC_JP) | 拡張UNIXコード日本語フォーマット |
JISAutoDetect | 読み込み(Reader)専用。日本語文字コードを自動判別 |
リスト9は、標準入力System.inをデフォルトの文字コードで文字ストリームに変換し、出力ストリームではEUC-JPに変換して出力しています。
▼リスト9
import java.io.*; class EncodingDemo { public static void main(String[] args) throws IOException { BufferedReader in = null; BufferedWriter out = null; try { in = new BufferedReader(new InputStreamReader(System.in)); out = new BufferedWriter(new OutputStreamWriter(System.out, "EUC-JP")); String str = in.readLine(); out.write("EUC-JP: " + str); out.newLine(); out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { in.close(); out.close(); } } }
Javaの勉強で最初にお目にかかるメソッド、System.out.pritln()は、"Hello ,World!"を表示させるだけではありません。SystemクラスのoutフィールドはPrintStream型です。
PrintStreamは、さまざまなデータを簡単に出力するクラスです。PrintStreamの文字ストリーム版がPrintWriterで、こちらは任意のデータを文字列に変換して文字ストリームに書き出します。
いずれのクラスも、フラッシュの制御、文字コードの指定をサポートしており、print(), println()の二つのメソッドで、いろいろなデータを簡易的に書き出すことができます(リスト10)。
▼リスト10
PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter("myFile.txt"))); out.println("Hello, World!"); out.close();
バッファ・サイズを指定するなどのカスタマイズで、I/O効率を向上させることができます。
FileInputStreamを例にとりましょう。read()メソッドは三つオーバーロードされています。
public int read() throws IOException public int read(byte[] b) throws IOException public int read(byte[] b, int off, int len)
read(byte[] b)では、byte[] bに対して最大でb.lengthバイト読み込むことができます。read(byte[], int off, int leng)では、byte[] bに対して、開始オフセットoffから最大でlenバイトだけ読み込めます。戻り値はバッファに読み込まれたバイト数です。データがない場合は-1になります。 FileOutputStreamでも同様のことが言えます。write(byte[] b), write(byte[] b, int off, int len)によって、バッファをカスタマイズできます(リスト11)。
▼リスト11
// バッファ・サイズの指定 int bufferSize = 10000; byte[] buffer = new byte[bufferSize]; // 入出力ストリームの作成 InputStream in = new FileInputStream(args[0]); OutputStream out = new FileOutputStream("copy_" + args[0]); // バッファに対する読み込み/書き出し int block; while((block = in.read(buffer)) != -1) { out.write(buffer, 0, block); } // ストリームのクローズ in.close(); out.close();
インタフェースSerializableかExternalizableを実装するクラスのオブジェクトは、ObjectInputStream/ObjectOutputStreamにより、オブジェクトをファイルやネットワークに書き出したり、復元したりできます。
オブジェクトの本体はフィールド(インスタンス変数)の値です。メソッドなどはクラス定義から復元できますが、ある時点でオブジェクトが保持しているフィールドの値はオブジェクト固有であり、永続化(persistent)するには、ファイルやネットワークなどJavaVMの外に書き出す必要があります。
オブジェクトをストリームに書き出すには、フィールドの値を連続するデータの連なりに変換する必要があります。これを直列化(シリアライズ)と呼びます。オブジェクトの直列化では、フィールドの値がプリミティブ型ならばそのまま書き出します。他のオブジェクトを参照している場合には、そのオブジェクトも直列化して書き出します。
直列化可能なオブジェクトは、SerializableかExternalizableを実装するクラスをインスタンス化したものです。Serializableは直列化可能を意味するだけで、なにも定義していません。ExternalizableはSerializableのサブインタフェースです。
直列化可能なオブジェクトの直列化/復元 は、ObjectOutputStream/ObjectInputStreamによる、読み込み/書き出し時に自動的に処理されます(リスト12)。
▼リスト12
// ObjectOutputStreamの作成 ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream("t.tmp"))); // 直列化 out.writeObject(obj); out.flush(); out.close(); // ObjectInputStreamの作成 ObjectInputStream in = new ObjectInputStream( new BufferedInputStream(new FileInputStream("t.tmp"))); // 復元(逆直列化) // 元の型にキャスト List inObj = (ArrayList)in.readObject(); in.close();
修飾子staticとtransientがついたフィールドは直列化されません。逆に、serialPersistentFieldsというObjectStreamField[]型のフィールドで明示的に指定することもできます。
直列化では、再帰的に参照するオブジェクトを書き出すために、ディスク容量、実行速度の両面で非常にコストの高い方法です。また、JavaVMの外側に書き出されたものは、Javaのセキュリティの枠組みから保護されないので、不正なアクセスに対して無防備になります。以上の点で、永続化する必要がないものは、できるだけ書き出さないことが重要です。
staticフィールドは、クラス定義によって復元可能なので直列化されません。
transient修飾子は、直列化したくないフィールドを明示するために指定します。transientが指定されたフィールドは書き出されないので、復元できるように、readObject()をオーバーライドしておく必要があります。
serialPersistentFieldsフィールドを指定する場合、この配列に何も追加しないと、何も保存されません。また、transientが指定されたフィールドでも、serialPersistentFieldsに追加されると書き出されます。
Serializableオブジェクトは自動的に直列化されます。挙動をカスタマイズする場合は、Externalizeの方が柔軟です。
Serializableオブジェクトは、復元するために十分な情報がデフォルトで保管できます。Externalizeオブジェクトでは、デフォルトでは、オブジェクトの型を識別する情報だけが保管されます。ここでオブジェクトの状態を保管/復元するためには、十分な情報を書き出すためのwriteExternal()メソッドと、オブジェクトを復元するためのreadExternal()メソッドを実装しておく必要があります。
Serializableの保管/復元のアーキテクチャを利用したくない/利用できないときには、Externalizeを使います。