オブジェクト指向プログラミングb 第6回


●入出力処理(中編)「もっとJavaを学ぶ 第4回 (2003年12月 )」

 今回は,引き続き入出力処理の基礎として,バイトデータ入出力系ストリームの使用例と,
文字データ入出力系ストリームの解説をする。文字エンコーディングの変換例も登場するの
で,しっかり勉強しよう。

【バイトデータ入出力系の使用例】
 前回は,バイトデータ入出力系のストリーム全般を解説した。今回はその続きとして,い
くつかのバイトデータ入出力系のストリームの使用例を紹介することから始めよう。

●DataInputStream/DataOutputStream
 まず,基本的なデータ型の値をバイトデータとして入出力する
  ・DataInputStream
  ・DataOutputStream
の使用例を見てみよう。
 List 1は,指定されたファイルにint型データとdouble型データをバイトデータとして書き
込んだ後,逆にファイルからデータを読み込む例である。List 1では,datastreamtest1.data
という名前のファイルに書き込むので,同名の重要なファイルがすでにある場合は,List 1で
読み書きするファイル名を,上書きしても良い名前に変更するように。
List 1 DataStreams.java

 まずDataOutputStreamを使った書き込み用メソッドから説明していこう。DataOutputStream
はラッパストリームだが,List 1の例ではまずFileOutputStreamストリームを生成し,それ
をBufferedOutputStreamでラップし,更にDataOutputStreamでラップしている(List 1-<①>,
<2>)。
 そして,オープンしたDataOutputStream型ストリームdosに対して,int型データとdouble
型データを書き込んでいるのが,List 1-<3>の
  dos.writeInt( i );
  dos.writeDouble( d );
という部分である。DataOutputStreamには,前回紹介したように,xxxxを基本データ型名と
すれば,
  writeXxxx()
というメソッドがあり,これでxxxx型データをストリームに書き出すことができる。List 1-<4>
では,データをすべて書き込んだ後で,
  dos.flush();
として,念のためストリームをフラッシュしていることに注意。なお,ラッパストリームを使っ
ている場合,フラッシュを行うには,一番外側でラップしているラッパストリームのflush()メソ
ッドを呼ぶようにせよ

 例外については,前回解説したようにIOException型例外が投げられる可能性があるので,catch
ブロックでキャッチするようになっている(List 1-<5>)。
 また,例外が発生したときも発生しないときも,ストリームを明示的にクローズするために
finallyブロックでclose()メソッドを呼んでクローズしている。このとき,前回説明したように,
ラッパストリームからクローズすること。List 1のようにストリームが多重にラップされていて
も,基本は同じで,より“外側からラップしている”ストリームから順にクローズするようにす
る。つまり,List 1-<6>にあるように,まずDataOutputStream型ストリームdosからクローズ
し,次にdosにラップされていたBufferedOutputStream型ストリームbofをクローズ,最後に
bofにラップされていた真性ストリームであるFileOutputStream型のストリームをfosクローズ
することになる。
 DataInpuStreamによるデータの読み込みでも,だいたい要点は同じである。まず,ストリー
ムのオープンだが,List 1の例では,書き込みの場合と同じように,ファイルストリームをオー
プンし,ついでバッファつきストリームでラップして,それをさらにDataInputStreamでラップ
している(List 1-<7>,<8>)。
 データの読み込みを実際に行っているのは,List 1-<9>の
  i = dis.readInt();
  d = dis.readDouble();
という部分である。前回紹介したように,DataInputStreamには,xxxxを基本データ型名とすれ
ば,readXxxx()というメソッドがあり,これによってデータ値の読み込みを行うわけである。実
際に読み込んだデータを表示しているのがList 1-<10>の部分である。
 読み込み時の例外の扱いも,書き込みの場合と同じで,必要が有ればcatchブロックでIOException
をキャッチし(List 1-<11>),finallyブロックで,ラッパストリームからクローズする(List 1-<12>)。
 実際に,main()メソッドの中で,ファイルへデータを書き込み,書き込んだデータを読み込ん
でいる部分が,List 1-<13>である。List 1を実行すると,
  i is 123, d is 123.456
と表示され,ちゃんとデータの読み書きが行われたことが確認できる。

●ObjectInputStream/ObjectOutputStream
 List 1でご覧頂いたように,基本データ型のストリーム入出力は,DataInputStream/
DataOutputStreamを使って実行できる。そして,オブジェクトの入出力を行うのが,
  ・ObjectInputStream
  ・ObjectOutputStream
である。これらのストリームを使用すれば,オブジェクトをファイルに保存したり,保存
したオブジェクトのデータをファイルから読み込んだりすることが簡単に実行できる。
 前回,簡単に紹介したが,オブジェクトを一連のバイトデータに変換してストリームに
書き込むことを,シリアライズ(直列化)といい,逆にシリアライズされたデータをストリ
ームから読み込んで,元のオブジェクトに復元することを,デシリアライズ(直列化復元)
という。
 前述したように,オブジェクトのシリアライズ(デシリアライズを含む)は,ObjectInputStream
とObjectOutputStreamを使用すれば手軽に行える。しかし,どんなクラスのオブジェク
トでもシリアライズできるわけではない。実は,特別なインタフェイスjava.io.Serializable
を実装しているクラスのオブジェクトだけが,シリアライズすることができる
のである。
 インタフェイスSerializableの定義自体は,
  public interface Serializable {}
というようになっていて,メンバを一切持っていない。このインタフェイスは,Javaのシ
ステムに対して,「このSerializableを実装しているクラスのオブジェクトはシリアライズ
できる」という目印(マーク)の役目を果たすだけなのである。そのため,クラスをシリアラ
イズ可能にするためには,単にSerializableを実装すればよく,特にメソッドの実装などを
行う必要はない
。Serializableように,クラスが特別な性質を持っていることを,Javaのシ
ステムに知らせるために使われる特別なインタフェイスのことを,マーカーインタフェイス
と呼ぶ。
 ではList 2を例に,実際にObjectInputStreamとObjectOutputStreamを使って,オブジ
ェクトを読み書きする方法を見てみよう。なお,List 2は,オブジェクトをobjectstreamtest1.data
というファイルに書き込むので,同名の重要なファイルがある場合は,上書きしてしまわ
ないように適当にファイル名を変更してから実行すること。
List 2 ObjectStreams.java

 List 2では,String型オブジェクトと自作クラスのオブジェクトをファイルに書き込んで
みよう。List 2-<①>は,“人”を表す自作クラスPersonの定義である。インタフェイスSerializable
を実装していることに注意。Personには,名前を格納するString型フィールドnameと年齢
を表すint型フィールドageがある。また,名前と年齢を表示して自己紹介を行うメソッド
introduce()がある。
 一方,StringはSerializableを実装しているので,そのままでシリアライズすることが可能
である。Java標準パッケージの代表的なクラスには,Serializableを実装しているものが多く
あり,シリアライズ可能になっている。
 さて,List 2-<2>から始まるwriteObjectTofile()メソッドが,オブジェクトを書き込むメ
ソッドである。String型変数sとPerson型変数pの2つの仮引数で,書き込むオブジェクトを
受け取る。List 2-<3>ではストリーム変数を宣言し,List 2-<3>では,ファイルストリーム
fosをラップする形で,ObjectOutputStream型ストリームoosを生成している。
 そして,実際に書き込んでいる部分は,List 2-<5>の
  oos.writeObject( s );
  oos.writeObject( p );
という部分である。このObjectOutputStreamのwriteObject()メソッドは,実引数に与えら
れたオブジェクトを,ストリームに書き出してくれる。実引数に与えられるのは,Serializable
を実装したクラスのオブジェクトだけなので,注意すること。
 オブジェクトを書き出したら,ストリームをフラッシュしておく(List 2-<6>)。そしてエ
ラー処理と後始末として,catchブロックで例外をキャッチし(List 2-<7>),finallyブロック
でストリームをラッパストリーム側からクローズしている(List 2-<8>)。
 List 2-<9>からは,オブジェクトをファイルストリームから読み込むためのメソッドreadObjectToFile()
を定義している。List2-<10>では各ストリーム型変数を宣言し,List 2-<11>で,ObjectInputStream
型オブジェクトoisを生成している。
 そして,List 2-<12>の
  String s=(String)ois.readObject();
  Person p=(Person)ois.readObject();
という部分でオブジェクトをストリームから読み込んでいる。1行目でString型オブジェクト
を,2行目でPerson型オブジェクトを読み込んでいる。このように,書き込んだ順番と同じ順
番でオブジェクトを読み込むようにしなくてはならない
ので注意すること。
 ここで登場したObjectInputStreamのreadObject()メソッドがオブジェクトを読み込むメソ
ッドで,Object型で読み込んだオブジェクトを返してくる。そのため,この例のようにreadObject()
の返り値を本来の型にキャスト(型変換)してから利用することに注意
。また,readObject()は,
適切にクラスを読み込めなかった場合に,ClassNotFoundException例外を投げるので,おぼ
えておこう。
 List 2-<13>で,読み込んだString型オブジェクトの内容を表示し,List 2-<14>では,読み
込んだPerson型オブジェクトのintroduce()メソッドを呼び出している。
 オブジェクトを読んだ後は,エラー処理と後始末として,例外をキャッチし(List 2-<15>,
<16>),ストリームをクローズする(List 2-<17)。
 main()メソッドでは,最初にPerson型オブジェクトp1を生成し(List 2-<18>),続いてList 2-<19>
で"hello"という文字列(String型オブジェクト)とp1をファイルに書き込んでから,読み込みを
行っている。
 List 2を実行すると,ファイルから読み込まれたString型オブジェクトの内容
  "hello!"
が表示され,ファイルから読み込まれたPerson型オブジェクトの自己紹介が表示されるはずであ
る。
 以上のように,オブジェクトの読み書きはかなり手軽に行うことが可能である。手軽にできる
とは言え,実際に実行されている内容はかなり複雑である。List 2の例でその一端を紹介しよう。
 Person型オブジェクトは名前を格納するString型フィールドnameを持っている。つまり,
Person型オブジェクトは,Fig. 1の左端に示すように,Stringオブジェクトを参照しているわけ
である。writeObject()メソッドは,その参照関係を保ったまま,シリアライズしてストリーム
に書き出している。そして,デシリアライズするときには,Fig.1に示すように,オブジェクト
の参照関係も復元してくれるのである。

 しかし,デフォルトのシリアライズ・デシリアライズ処理が,期待したよりも処理に時間がか
かったりすることもある。また,特定のケースでは,デフォルトのシリアライズ・デシリアライ
ズでは,うまくいかないこともある。そのような場合のために,シリアライズ・デシリアライズ
の動作をカスタマイズする手段も用意されている。ここでは詳しくは触れませんが,興味がある
方は調べてみるといいだろう。

【文字データ入力ストリーム】
 前回と今回の前半は,バイトデータ入出力ストリームを中心に解説してきた。ここからは,文字
データ入入力ストリームについて,紹介することにしよう。
 Fig.1に,文字データ入力ストリームのクラス間関係を示す。前回紹介したバイトデータ入力スト
リーム群のクラス間関係と比較すると,多くの類似点が見て取れる。文字データ入力ストリームも,
バイトデータ入出力ストリームと同じように,大きく真性ストリームとラッパストリームにわける
ことができる



では,それぞれのクラスを簡単に紹介していこう。

●スーパークラスReader
 すべての文字データ入力ストリームのスーパークラスとして,抽象クラスReaderが用意されて
いる。このため,文字データ入力ストリームのことを,単に“リーダ”と呼ぶこともある。抽象
クラスReaderのメソッドをTable 1に示す。 いくつかのメソッドは,バイト入力ストリーム群の
スーパークラスであるInputStreamの同名メソッドと同じ動作意味を持っている。
 Readerで特徴的なのは,read()メソッドである。これらによって,文字データを読み込むこと
になる。


●文字入力用の真性ストリーム
 文字入力用の真性ストリームには,Fig.2のように,4つのストリームが用意されている。

・StringStream
 まず,StringStreamだが,これは文字列を入力元とするリーダである。コンストラクタをTable
2-(1)
に示す。

・CharArrayReader
 CharArrayReaderは,char型配列を入力元とするリーダである。コンストラクタをTable 2-(2)
に示す。

・PipedReader
 PipedReaderは,スレッド間のデータのやりとりに使われるリーダである。これに関しては本授
 業では詳しく解説しない。

・FileReader
 FileReaderは,ファイルを入力元とするリーダである。コンストラクタをTable 2-(3)示す。なお,
Fig.2に示すようにFileReaderは,後述するInputStreamReader(Table 2-(4))のサブクラスなので,
そのメソッドを継承していることに注意。

●文字入力用のラッパストリーム
 Fig.2に示すように,文字入力用のラッパストリームには,
 ・InputStreamのラッパストリーム
 ・Readerのラッパストリーム
の2系統がある。まず,前者に属するInputStreamReaderから紹介しよう。

・InputStreamReader
 InputStreamReaderは,InputStreamをラップするリーダである。このことに違和感を憶えた人
もいるのではないだろうか。文字データを読むなら,Readerをラップするのが当然に思えるのに,
バイトデータ入力系のInputStreamをラップするなんて,どういうことだろう,と。
 これには,理由がある。Javaのプログラム内では,文字データは1文字2バイトのUnicodeで表現
されている。しかし,様々なコンピュータでは,文字データをいろいろな文字エンコーディングで
扱っている。そのため,ストリームの入力元(たとえばファイル)からやってくる文字データは,かな
らずしもUnicodeで表現されているとは限らない
のである。
 この問題を解決するために用意されているのが,このInputStreamReaderなのである。ストリー
ムの入力元がUnicode以外の文字エンコーディングを採用している場合,ストリームからやってくる
データは,もはや文字データとは言えず,バイトデータとかわりないことになる。そのため,バイト
データを入力するInputStreamでバイト単位でデータを読む必要があるのである。そして,InputStreamReader
は,InputStreamをラップして,その出力をUnicode文字に変換してくれる
のである(Fig.3)。

 ファイルは,このような文字エンコーディングの違いが問題になりやすいので,FileReaderは
InputStreamReaderのサブクラスになっている(Fig.2)
。そのため,実はFileReaderはInputStream
のラッパなのである(実際は,InputStreamのサブクラスであるFileInputStreamをラップする)。
 しかし,利用者としての立場からは,FileReaderがラッパストリームであることを意識しなくて
よいので,Fig.2では真性ストリームに分類してある。
 InputStreamReaderのコンストラクタと独自メソッドをTable 2-(4)に示す。コンストラクタは,
ラップするInputStreamオブジェクトを指定する第1引数inを持っている。また,第2引数ではInputStream
から入力されるバイトデータの文字エンコーディングを指定できる。
 文字エンコーディングを指定するには,Charset型オブジェクトか,文字エンコーディング名を
表す文字列を使う。また,文字エンコーディングを変換するデコーダ(CharsetDecoder型オブジェ
クト
)を指定することも可能である。しかし,CharsetやCharsetDecoderは,Java 2 SDK 1.4から
導入された新しい入出力の仕組みであるnioパッケージに属するクラスである。nioパッケージは,
本授業では解説しないので,これらのクラスについては詳しくは触れません。
 したがって,ここでは文字エンコーディング名を指定する方法が中心になるので,Javaでサポー
トする文字エンコーディングのうち,主要なもののエンコーディング名をTable 3に示す。なお,
Java SE SDKドキュメント(http://java.sun.com/j2se/1.4.2/docs/,日本語版はこちら)の「国
際化(Internationalization )」に,サポートされているエンコーディングの項目があるので,詳しく
はそのページを参照せよ。

 なお,コンストラクタで文字エンコーディングを指定しない場合,デフォルトのエンコーディン
グが指定されたことになる。デフォルトのエンコーディングは,プラットフォームだけではなく,
Javaのバージョンによっても若干変わる。MacOS X 10.2上のJava 2 SDK 1.4.1_01ではデフォル
トのエンコーディングはSJIS,Windows XP上のJava 2 SDK 1.4.2では,MS932である。なお,
そのストリームのエンコーディングを知りたい場合は,Table 2-(4)getEncoding()メソッドが
利用できる。

・FilterReader
 次に,Readerのラッパストリームを紹介していこう。FilterReaderは,リーダをラップ
することでフィルタ機能を持たせたラッパリーダである。Table 2-(5)にコンストラクタを
示す。

・PushbackReader
 PushbackReaderは,FilterReaderのサブクラスで,プッシュバック機能を持つラッパリ
ーダである。主なコンストラクタとメソッドをTable 2-(6)に示す。

・BufferedReader
 BufferedReaderは,リーダをラップすることでバッファ機能を持たせたラッパリーダで
ある。主なコンストラクタとメソッドをTable 2-(7)に示す。特徴的なのは,行単位の入力
を行うメソッドreadLine()を持つことである。

・LineNumberReader
 LineNumberReaderは,BufferedReaderのサブクラスで,行番号を認識するラッパリー
ダである。主なコンストラクタとメソッドをTable 2-(8)に示す。

【文字データ出力ストリーム】
 次に,文字データ出力ストリームの概要を見てみよう。Fig.4に,文字データ出力ストリー
ムのクラス間関係を示す。前回紹介したバイトデータ出力ストリーム群のクラス間関係と比較
すると,多くの類似点が見つかるはずである。文字データ出力ストリームも,バイトデータ入
出力ストリームと同じように,大きく真性ストリームとラッパストリームにわけることができ



●スーパークラスWriter
 すべての文字データ出力ストリームのスーパークラスとして,抽象クラスWriterが用意され
ている。このため,文字データ出力ストリームのことを,単に“ライタ”と呼ぶこともある。
抽象クラスWriterのメソッドをTable 4に示す。メソッドclose()とflush()は,バイト出力スト
リーム群のスーパークラスであるOutputStreamの同名メソッドと同じ動作意味を持っている。
 Writerで特徴的なのは,write()メソッドである。write()メソッドによって,文字データを
書き込むことになる。


●文字出力用の真性ストリーム
 では,文字データ出力ストリーム群のうち,まずは真性ストリームから解説していこう。

・StringWriter
 StringWriterは,StringBuffer型の文字列バッファを出力先とするライタである。コンストラ
クタと独自メソッドをTable 5-(1)に示す。

・CharArrayWriter
 CharArrayWriterは,char型配列を出力先とするライタである。出力先の配列は,必要があ
れば自動的に拡大される。コンストラクタと独自メソッドをTable 5-(2)に示す。

・PipedWriter
 PipedWriterは,スレッド間のデータのやりとりに使われるリーダである。これに関しては
本授業では詳しく解説しない。

・FilterWriter
 FilterWriterは,ファイルを出力先とするライタである。コンストラクタをTable 5-(3)に示
す。なお,Fig.4に示すようにFilterWriterは,後述するOutputStreamWriter(Table 5-(4))の
サブクラスなので,そのメソッドも継承していることに注意するように。

●文字出力用のラッパストリーム
 文字出力用のラッパストリームは,バイトデータ出力ストリームのoutputStreamをラップ
するものと,Writerをラップするものの2種類がある。まず,前者から見ていこう。

・OutputStreamWriter
 OutputStreamWriterは,InputStreamReaderの出力版で,OutputStreamをラップして,
Unicode文字データから,別のエンコーディングへ変換したデータを書き込む機能を持ってい
るライタである。コンストラクタと独自メソッドをTable 5-(4)に示す。
 上述したように,FilterWriterはこのOutputStreamWriterのサブクラスなので,厳密に言え
ば真性ストリームとは言えない。しかし,FilterWriterは指定されたファイルをFileOutputStream
で開いて,それをラップする形で出力を行うので,利用者はラッパライタであることを意識す
る必要はない。そのため,Fig.4では,FilterWriterを真性ストリームに分類している。

・FilterWriter
 次に,Writerのラッパライタを見ていこう。まず,FilterWriterだが,これはライタをラップ
することでフィルタ機能を持たせたラッパライタである。Table 5-(5)にコンストラクタを示す。

・BufferedWriter
 BufferedWriterは,ライタをラップすることでバッファ機能を持たせたラッパライタである。
主なコンストラクタとメソッドをTable 5-(6)に示す。行末記号を書き込むメソッドnewLine()
がある。

・PrintWriter
 PrintWriterは,PrintStreamの改良版として導入された。Table 5-(7)に,コンストラクタと
独自メソッドを示す。このストリームは,Writerだけでなく,OutputStreamもラップすること
ができる。OutputStreamをラップする場合は,まずOutputStreamをラップするOutputStreamWriter
を生成して,それをさらにラップするようになる
。このとき,文字を書き込むと,Unicode文字
データをデフォルトのエンコーディングへ変換してから出力することになる。print()メソッドや
println()メソッドで,気軽にオブジェクトや基本データの値を出力するのに使用できる。

【文字データ入出力ストリームの使用例】
 では,文字データ入出力ストリームの簡単な使用例を示そう。List 3は,文字データ入
出力ストリームを使って,ファイルに特定の文字エンコーディングで文字データを書き込
む例である。WriteReadTest1.dataという名前のファイルに書き込むので,同名の大事な
ファイルがある場合は,コンパイルする前に適当に別の名前を指定するように。
List 3 WriteReadTest.java

 まず,ファイル名nameのファイルにエンコーディングcharsetNameで書き込み,また
読み込むメソッドwriteToFileReadFromFile()が定義されている。その中では,まず,ス
トリーム変数を宣言している(List 3-<①>)。List 2-<2>ではファイル名nameのファイルを,
FileOutputStreamでオープンし,List 3-<3>ではそれをOutputStreamWriterでラップす
る(エンコーディングはcharsetNameで指定される)。ついでさらに,BufferedWriterでラ
ップする(List 3-<4>)。
 この例のように,OutputStreamWriterやInputStreamReaderは文字コード変換を行う
ので,バッファつきのラッパでラップすると入出力が効率よくなる
ので,おぼえておこう。
 そして,List 3-<6>で文字列をwrite()メソッドで書き込む。このとき,Unicode文字列
が,charsetNameで指定されたエンコーディングでファイルnameに書かれることになる。
さらに,List 3-<7>では改行文字を書き込んでいる。書き込みが終わったら,念のためフ
ラッシュする(List 3-<7>)。
 List 3-<8>はキャッチブロックだが,UnsupportedEncodingExceptionはIOException
のサブクラスなので,IOExceptionよりUnsupportedEncodingExceptionを先にキャッチ
していることに注意。finallyブロックではストリームをクローズする(List 3-<9>)。
 次に読み込みである。List 3-<10>で読み込み用ストリームの変数を宣言している。
List 3-<11>〜<13>で,ファイルをオープンし,自動エンコーディング検出の"JISAutoDetect"
を指定したInputStreamReaderでラップし,さらにBufferedReaderでラップしている。
 そして,List 3-<14>のreadLine()で1行分の文字列を読み込む。このとき,readLine()
が返すのはUnicode文字列だが,println()メソッドによって,プラットフォームのデフォル
トエンコーディングへ変換されて出力されることになる。
 List 3-<15>のcatchブロックの注意点は,前述のとおり。finallyブロックでは,ストリ
ームをクローズする(List 3-<17>)。
 List 3を実行すると,List 3-<17>のメソッド呼び出しでは,ファイルへ書き込む際の文
字エンコーディングとして日本語EUCが指定されている。実行すると,
  こんにちは。文字列を書き込んでいます。
と表示される。ファイルWriteReadTest1.dataの内容は日本語EUCになっているので,文字
エンコーディングがわかるテキストエディタなどで開いて確認してみるといいだろう。

【次回は】

 次回もひきつづき入出力について解説していく。




戻る