プログラミング応用b 第7回『ファイル入出力の基礎』07-03. 『文字データ入力系ストリーム』


 では,文字データ入出力系ストリームの解説をする。文字エンコーディング(文字コード)の変換例
も登場するので,しっかり勉強しよう。また,ファイルを配列の様に扱うランダムアクセスファイル
の基本も学習する。最後に,ファイルを扱う上で便利なクラス File と FileDescriptor について
も紹介する。


■3. 『文字データ入力系ストリーム』

 入出力ストリームを表すクラスを文字通り「ストリーム」と呼ぶことがある。
Fig.1に,文字データ入力ストリームを表すクラス群の関係を示す。
これらのクラスは java.io パッケージ所属である



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


■3.1. 『スーパークラスReader』

 まず,すべての文字データ入力ストリームのスーパークラスとして,抽象クラス Reader が用意されて
いる。このため,文字データ入力ストリームのことを,単に“リーダ”と呼ぶこともある。Readerは,
文字データ入力ストリームが備えるべき基本的なメソッドを定義している抽象クラスである。
つまり,Fig.1 に示した Readerの全サブクラス(リーダ)はこれらのメソッドを持っている。

抽象クラス Reader のメソッドを Table 1 に示す。重要なメソッドとしては,使い終わったストリーム
をクローズする close メソッド,ストリームから文字を読み込む複数の readメソッド がある。
表下部に注記してあるように,これらReaderのメソッドはエラーが起こって本来の目的が達成できな
かった場合は,IOException型例外を投げることにも注意しよう。


 その他,入力データを指定した数だけ読み飛ばす skip メソッドもある。
 また,入力ストリームによっては,マーク機能を持つものもある。

コラム
 マーク機能とは,ストリームからデータを入力しているとき,マークを設定すると,後でそのマーク位置
まで戻って再びデータを再入力できるようにする機能である。
mark( )メソッドを呼び出すと,現在の読み込み
位置にマークが設定される。そして,
reset( )メソッドを呼び出すと,最後に設定したマーク位置まで読み込み位置
が戻され,そこから再度読み込みが開始される。
 実際には,マークを設定した時点からreset( )が呼び出されるまでに入力したデータをストリームオブジェクトが
保存しておくことで,このマーク機能は実現されている。最大どれだけのデータを憶えておくようにするか指定で
きるmark( )メソッドも用意されている。すべてのストリームでマーク機能が使えるわけではなく,マーク機能を
持っているストリームでは,
markSupported( )メソッドがtrueを返すようになっている。



■3.2. 『基本ストリームとラッパストリーム』

 では,Fig. 1 にもどって,今度は Reader のサブクラスを見てみよう。Fig.1 を再掲する(下図をクリック
すると授業で扱わないクラスは非表示になる)。



 Reader のサブクラスは,2つのグループわけられる。ひとつは,本当の意味でのストリームで,
文字データの入力源(キーボード・通信路・ファイルなどのハードウェア装置,文字列・文字配列
などの文字を格納しているデータ構造)と直接接続
するストリーム
である。ここでは便宜的に
基本ストリーム”と呼ぶことにする(下図)。



 もうひとつのグループは,他のストリームの後段に珠々繋ぎのように接続されて,ストリームに
付加機能をつけるためのストリーム
で,ここで便宜的に"ラッパストリーム(wrapper stream)"と呼ぶ
ことにする。

単独のラッパストリームオブジェクトの特徴を挙げると下図の様になる。




このように,ラッパストリームは Reader型参照値で入力元のリーダ(基本ストリームか他のラッパストリーム)
を珠々繋ぎにできる(下図)。



ラッパストリームを使ってリーダを珠々繋ぎにする,という仕組みの応用例を下図に示す。文章が多いように
見えるが,独習のために細かく書いてあるだけで授業での解説を聞けばアイディアはそれほど複雑ではない
ことが分かるだろう。



以上の,ラッパストリームに関する図(1)〜(4)をひとつにまとめた図はこちら

ここで,入力ストリームオブジェクト(リーダ)を初期化し,そのストリームをオープンする
コンストラクタの引数
について以下の表にまとめておく。

文字入力ストリーム(リーダ)の種類 コンストラクタへの引数
基本ストリーム 入力源を指定する情報 (例:ファイルを入力源とするFileReaderクラス
の場合,ファイル名などファイルを指定する情報)
ラッパーストリーム 入力元となるストリームオブジェクトへのReader型参照値。

 


■3.3. 『文字入力用の基本ストリーム』

 では,文字入力用の基本ストリームから見ていこう。文字入力用の基本ストリームには,Fig.1(再掲)のように,
4つのストリームが用意されている。授業では,このうち InputStreamReader FileReader のみ扱う。




■ InputStreamReader

 InputStreamReader は,ファイルを入力源とする基本ストリーム(リーダ)である。下図に InputStreamReader型リーダ
オブジェクトの「ファイルを入力源として直接接続した」概念的な姿とその真の姿を示す。InputStreamReader型リーダは
基本ストリームのはずなのに,ラッパストリームとして実現されているのは,文字データには,文字コード(文字エンコーディング)
の違いという問題があるためである。




このように,InputStreamReaderの実際の働きはラッパストリーム(直接接続されるデータ入力元が入力源ではない)なのだが,
文字データストリームの事実上の基本ストリームとして扱ってかまわない。

InputStreamReaderの主なコンストラクタと主なメソッドを下表に示す。

InputStreamReader の主なコンストラクタ
※実態はラッパストリームなので,(InputStream型の)入力元ストリームオブジェクトへの参照値を(第1)実引数に取る
 ※第2実引数として,第1実引数で指定された入力元ストリームの仮定される文字コードを指定する情報を取る
これらの実引数によって初期化されストリームをオープンする。

 InputStreamReader( InputStream in,                String charsetName )

 InputStreamReader( InputStream in )

InputStream型ストリーム in を入力元とするストリームを生成・初期化する。

・文字エンコーディングを文字列 charsetName で指定すれば,inからの入力されたバイトデータを指定
 されたエンコーディングの文字として解釈して読み込み,Javaの内部文字コード『UTF-16 ビッグエンディアン』
 の文字へ変換した結果をread( )メソッドで返すようにする。
 
・文字コーディングが指定されない場合は,デフォルトエンコーディング(Javaの使用環境で設定されている
 入力源の文字エンコーディング)
が指定されたものとみなされる。

 デフォルトエンコーディングは,System.getProperty("file.encoding") が返す文字列で知ることができる。
 本授業でインストールした Eclipse (2023-12)では,"UTF-8" となっているはずである。
 
※なお,エンコーディング名を指定するコンストラクタは,指定されたエンコーディングがサポートされて
 いないときは,UnsupportedEncodingException (IOExceptionのサブクラス) 型の例外を投げる。


※第2実引数で指定できる文字エンコーディングの例。
 特に日本語の文字データを入力する際は,文字エンコーディングを自動判別してくれる "JISAutoDetect" が便利


InputStreamReader の主なメソッド


 本授業では,上で紹介した Reader から継承したメソッド だけ知っていれば良い。


■FileInputStream

 なお,( InputStreamReaderの入力源である) InputStream には,接続する実際の入力源ごとにサブクラスが用意されている
ここで,バイトデータ入力系ストリームのうち,今回話しに登場するクラスの継承関係の図を以下に示す(図をクリックすると
イトデータ入力系ストリームのすべてのクラスを表示する)。これらのクラスは java.io パッケージ所属である
下図でもわかるとおり,ファイルをデータ入力源とするバイトデータ入力系の基本ストリームとしては,FileInputStream
定義されているので
,ファイルから文字を読み込むときにはこれをInputStreamReaderの入力元として利用する。


FileInputStream のコンストラクタを下表に示す。

FileInputStream のコンストラクタ
※基本ストリームなので入力源(ここではファイル)を示す情報をコンストラクタの実引数として渡してストリームをオープンする。
 FileInputStream ( String name )

 FileInputStream ( File file )

 FileInputStream ( FileDescriptor fd )

これらのコンストラクタは,
 ・String型のファイルパス名文字列 name
 ・File型オブジェクト file
 ・FileDescriptor(ファイル記述子)型オブジェクト fd
で指定されたファイルをバイトデータの入力源とするバイトデータ入力の基本ストリームをオープンする。

※指定されたファイルが存在しなかったり,ディレクトリだったりなどの理由でオープンできないときは,FileNotFoundException (IOExceptionのサブクラス) 例外を投げる。
※セキュリティ上の制限で読み込みアクセスが拒否された場合,SecurityException (RuntimeExceptionの
 サブクラス ) 例外が投げられる

※File型とFileDescriptor型については後で紹介する。


■ FileReader

 FileReader は,InputStreamReader のサブクラスで,
 ・入力源をファイルとする (つまり,指定されたファイルを入力源とするFileInputStream型オブジェクトを自動的にオープンして入力元とする)
 ・入力源の文字エンコーディングをデフォルトエンコーディングであると仮定して入力を行う。
というクラスで,簡易的にファイルから文字入力できる。

  ただし,FileReader は上記の様に入力源の文字エンコーディングをデフォルトエンコーディングであると仮定しているので,(デフォルトエン
コーディングの設定を変更しない限り)デフォルトエンコーディングで指定された文字コードで書かれたテキストファイルしか読めないので注意
しよう。

FileReader のコンストラクタを Table 2-(3) および以下の表に示す。

FileReader のコンストラクタ
 FileReader( String name )

 FileReader( File file )

 FileReader( FileDescriptor fd )
これらのコンストラクタは,
 ・String型のファイルパス名文字列 name
 ・File型オブジェクト file
 ・FileDescriptor(ファイル記述子)型オブジェクト fd
で指定されたファイルを入力源とする文字入力の基本ストリームをオープンする。実際には上図(2)のように指定されたファイルを入力源としたFileInputStream 型基本ストリームをオープンし,それをラップするFileReader型オブジェクトを生成・初期化する。
  指定されたファイルが存在しなかったり,ディレクトリだったりなどの理由でオープンできないときは,FileNotFoundException 例外を投げる。

※File型とFileDescriptor型については後で紹介する。
※すべてのコンストラクタは,エラー時に IOException 例外を投げる可能性がある。

FileReader の主なメソッドは以下の通り。
FileReader の主なメソッド


 本授業では,上で紹介した Reader から継承したメソッド および InputStreamReader から継承したメソッド
 だけ知っていれば良い。

以下に,FileReader を利用した簡単なデフォルトエンコーディングによるテキストファイル読み込みの例を示す。
テキストファイル「AandB.txt」をダウンロードして,Eclipse で作成したプロジェクトフォルダの中に入れて実行してみよ。
本授業のインストールガイドでEclipseをインストールしていれば,デフォルトエンコーディングはおそらく UTF-8
である。そして,「AandB.txt」は UTF-8 で「AとBと」書かれているので,正しく読めるはずである。

ソースファイル:FileReaderTest.java
import java.io.*;

class FileReaderTest {
    
    public static void main ( String [ ] args ) throws IOException {
        
        FileReader fr = null;
           
        try {
            fr = new FileReader( "AandB.txt" );
            int c = fr.read( ); // テキストファイルの中身の文字コードがデフォルトエンコーディングと同じと仮定して読み込む。
            while( c != -1 ) {  // readメソッドはストリーム終端に達して文字データを読めなかった場合は -1 を返すことを利用する。
                System.out.print( (char) c ); // readメソッドは int型として読み込んだ文字データを返してくるので,文字として扱う前にキャスト演算子でchar型に型変換する。
                c = fr.read( );
            }
        }
        catch( IOException ioe ) {
            System.out.println( ioe );
        }
        finally {
            if( fr != null ) fr.close( );
        }
        
    }

}

上記の例の

    int c = fr.read( );
    while( c != -1 ) {
        System.out.print( (char) c );
        c = fr.read( );
    }

の部分は,代入式自体が「代入された値」を式の値として持つ(例:a = 10 の場合,a = 10 という式自体が 10 という値を持つ)
ことを利用すれば,

    int c;
    while( ( c = fr.read( ) ) != -1 ) {
        System.out.print( (char) c );
    }

と書くこともできる。

以上の様に,FileReaderはテキストファイルの内容を簡単に読むことができる。しかし,
 ・デフォルトエンコーディングの文字コードで書かれているテキストファイルしか読めない。
 ・後述する BufferedReader が持つ readLineメソッドのような行単位の読み込みをする便利なメソッドを持っていない。
などの欠点を持っている。

本授業では,テキストファイルを読む方法としては,この後
 3.5. 『テキストファイルの読み出し例』
で解説する,様々な文字エンコーディングのファイルを読む方法を推奨したい。


■3.4. 『文字入力用のラッパストリーム』

 Fig.1 に示すように,文字入力用のラッパストリームには,

 ・InputStream のラッパストリーム
 ・Reader のラッパストリーム

の2系統がある。前者に関しては,基本ストリームの一部として紹介済みなので,後者のうち,授業で使用する
BufferedReader だけ紹介する。

■BufferedReader

 BufferedReader は,元の入力ストリームにバッファ機能を付加するラッパストリームである。
ディスク装置などの入出力機器のデータアクセス速度は,メモリ上のデータへのアクセス速度にくらべ,
圧倒的に遅い。そのため,少量のデータを頻繁に読み込むなどすると,非常に時間がかかってしまう(下図(1))。


 そこで,バッファ(buffer, 緩衝地帯という意味)と呼ばれるメモリ上の領域に,まとまった量のデータ
をあらかじめ一気に読み込んでおき, 実際に読み込むときには,そのバッファからデータを入力する
ようにすれば,入力処理を高速化することができる(上図(2))。

 BufferedReader オブジェクトは,バッファ領域を確保して,バッファ機能を提供してくれるラッパストリーム
なのである。積極的に使うべきラッパストリームと言える。主なコンストラクタとメソッドを下表とTable 2-(7)
に示す。特徴的なのは,非常に便利な行単位の入力を行うメソッド readLine を持つことである(下表)。

BufferedReader のコンストラクタ
 BufferedReader( Reader in )

 BufferedReader(Reader in, int sz)


Reader型ストリームオブジェクト in をラップするようにするコンストラクタ。バッファサイズを第2引数szで指定することもできる。


BufferedReader のメソッド
※Readerから継承したメソッド(read等)の他に,以下の特徴的なメソッドを持つ。

 String readLine( )



1行分の文字列(行末含まず)を読んで返す。改行文字(\n),復帰文字(\r),
または連続した改行復帰の2文字(\n\r)を行区切りとして認識する。
入力ストリームの終わりに達している場合は null を返す。

※入力元からバッファへ一度に多くの文字を読み込むという特徴を活かして
 行単位で文字列を返す機能を実現している。
※入出力エラーが発生した場合は,IOException例外を投げる。




■3.5. 『テキストファイルの読み出し例』

 では,実際にテキストファイルから文字データを読み込んでみよう。

まず,Eclipseで Javaプロジェクトを新規作成する。次に読み込む対象のファイル AandB.txt をダウンロードして,
Eclipseのパッケージエクスプローラー内の作成したプロジェクトフォルダ内にドラッグ&ドロップしてコピーする。
プロジェクトフォルダの『src』フォルダと並ぶ形で登録できればOK。

作成したJavaプロジェクトに以下のソースプログラム TextReading.java を作成して実行してみよ。

ソースプログラム:TextReading.java



※まず,入出力ストリームの機能はほとんどエラーが起こったときに例外を投げるので,例外の枠組みを使って記述することに注意しよう。
①:java.io パッケージをインポートする。
②:mainメソッドに IOException例外を投げることを明示する例外指定を書いている。理由は後述する。
③:tryブロックの前で,使用するストリームオブジェクトを参照するための変数をnull を初期値として宣言しておく。この例では,
   FileInputStream, InputStreamReader, BufferedReader
 を使用する。
④:まず,入力源であるテキストファイルと接続するFileInputStream型の基本ストリームをオープンする。入力源の情報としてコンストラクタ
  への実引数としてテキストファイル名を渡していることに注意。以下の様な状態になる。


⑤:入力元として④でオープンしたFileInputStream型ストリームへの参照値 fis と,文字エンコーディング"UTF8"を,コンストラクタの
  実引数として渡し,InputStreamReader型ストリームをオープンしている。以下の様な状態になる。


⑥:入力元として⑤でオープンしたInputStreamReader型ストリームへの参照値 isr をコンストラクタの実引数として指定し,BufferedReader
 型ストリームをオープンする。以下の様な状態になる。これで,テキストファイルから文字データを行単位で読み込む準備が整った,。


⑦:BufferedReader型ストリーム br の readLineメソッドを呼び出して1行分読み込んで表示する。"AとBと"と表示されるはずである。
⑧〜⑨:発生する可能性のある例外をキャッチしてエラーへの対処を行う。ここでは単にキャッチした例外オブジェクトを表示しているだけ。
⑩:tryブロックの中の処理で例外が発生する・しないに関係無く,finallyブロック内で使い終わったすべてのストリームを入力源から遠い
  ラッパストリームから順にクローズしていく
。 一番入力源の遠いラッパストリーム BufferedReader型ストリーム br のcloseメソッドだけ呼べばいいと
 思うかもしれないが,もし BufferedReader型オブジェクトをオープンしようとすところでエラーが起こった場合,
    if( br != null ) br.close( );
 だけでは, FileInputStream型ストリームと InputStreamReader型ストリームはクローズされないことになってしまう。
※③で,tryブロックの前でストリーム参照用の変数を宣言したのは,finallyブロックでこれらの変数を使用するため。
※②で,mainメソッドに IOException例外の例外指定をしたのは,finallyブロック内で呼び出す close メソッドが IOException例外を投げる
 可能性があるため。このままでは, finallyブロック内で呼び出す close メソッドで例外が発生した場合は,mainメソッドすなわちプログラム
 自体が強制終了するため,それを防ぎたい場合は,try-catch-finallyブロックをさらに tryブロックで囲み,この外部のtryブロックで
 IOException例外をキャッチするようにしよう。

なお,この例で,ファイル内容をすべて読み込んで表示する場合,

 ・BufferedReaderのreadLineメソッドは,入力ストリームの終わりに達して読み込めなかった場合は null を返す

という性質を利用して,⑦の部分を

String aLine = br.readLine( );
while( aLine != null ) {
    System.out.println( aLine );
    aLine = br.readLine( );
}

とすれば良い。また,代入式自体が「代入された値」を式の値として持つ(例:a = 10 の場合,a = 10 という式自体が 10 という値を持つ)
ことを利用すれば,

String aLine;
while( (aLine = br.readLine( ) ) != null ) {
    System.out.println( aLine );
}

と書くこともできる。





■3.6. 『ストリーム入出力処理の手順パターン』

 前節で見たように,ストリームを使った入出力処理の手順にはパターンがあり,まとめると以下の様になる。
ストリームを使った入出力を行うときは,この手順に従って処理を書けば良い。





■3.7 キーボードからの文字列入力 (2024年度はこの項目は試験に出ません)

 ここで,キーボードからの文字列入力方法を紹介する。

(1) キーボードからの文字列入力の正式な書き方の例

  標準入力ストリーム System.in (オープン済みのInputStream型ストリームで,プログラム動作中にファイルやキーボード入力
 などの入力源を切り替えることができる。通常はキーボード入力に接続されている)を入力源として
,InputStreamReader型基本ストリーム
 をオープンし,それを入力として BufferedReader型ストリームでラップする(BufferedReaderの行単位入力メソッド readLine が使いたい!)。

ソースファイル:KeyInputTest.java
import java.io.*; 

class KeyInputTest {
    
    public static void main ( String [ ] args ) throws IOException {
        
        InputStreamReader isr = null; BufferedReader br = null;
        
        try {
            // 標準入力ストリーム System.in (通常はコンソールからのキーボード入力に接続されている)を入力源として
            // InputStreamReader型ストリームをオープンし,それを入力して BufferedReader型ストリームをオープンする。
            isr = new InputStreamReader( System.in ); 
            br  = new BufferedReader( isr ); 
            
            System.out.println( "キーボードから文字列を入力して改行して下さい。空行(何も入力せずに改行のみ行う)を入力したら繰り返し入力を終了する。" );
            // BufferedReader型ストリームの readLineメソッドを呼び出すと,コンソールウインドウでキー入力待ち状態に
            // なるので,キーボードで文字列をタイプして改行する(ENTERキーを押す)。すると,キーボードから入力した
            // 1行分の文字列を String型文字列として返してくれる。
            String aLine = br.readLine( ); 
            while( ! aLine.equals( "" ) ) { // 空行が入力されたら繰り返し入力を終了する。否定の演算子 ! に注意。
                System.out.println( "入力された文字列:" + aLine );
                aLine = br.readLine( );
            }
            System.out.println( "入力は終了しました。" );
        }
        catch ( IOException ioe ) {
            System.out.println( ioe );
        }
        finally {
            if( br  != null )  br.close( );
            if( isr != null ) isr.close( );
        }
        
    }

}

Eclipse上で実行した様子を下図に示す。


ここで,

    String aLine = br.readLine( ); 
    while( ! aLine.equals( "" ) ) {
        System.out.println( "入力された文字列:" + aLine );
        aLine = br.readLine( );
    }

の部分は,代入式自体が「代入された値」を式の値として持つ(例:a = 10 の場合,a = 10 という式自体が 10 という値を持つ)
ことを利用すれば,

    String aLine;
    while( ! ( aLine = br.readLine( ) ).equals( "" ) ) {
        System.out.println( "入力された文字列:" + aLine );
    }

と書ける。


(2) キーボードからの文字列入力の簡易的な方法

 java.util パッケージの Scanner クラスを使用する。Scannerはストリームではないが,
  ・コンストラクタの実引数として入力源を指定してオープンする。
  ・使用し終わったら close メソッドを読んでクローズする必要がある。
という点では,文字データ入力の基本ストリームに似ている部分が有る。

Scannerを使うと,

 ファイル・ImputStream型ストリーム(キーボード入力(標準入力)含む)・String型文字列

などを入力源として文字列を読み込み,文字列や数値などを読み込むことができる。
非常に高機能なので,今回は1行分の文字列を読み込む例だけを示す。実行の様子は(1)と
同じである。

ソースファイル:ScannerTest.java
import java.util.Scanner;

class ScannerTest {
    
    public static void main ( String [ ] args ) {
 
        System.out.println( "キーボードから文字列を入力して改行して下さい。空行(何も入力せずに改行のみ行う)を入力したら繰り返し入力を終了します。" );
 
        Scanner scanner = new Scanner( System.in ); // 標準入力(普段はコンソール画面のキーボード入力に接続している)を入力源としてスキャナをオープンする。
        String aLine = scanner.nextLine( ); // Scanner の nextLineメソッドを呼び出すと,1行分の入力待ち状態になり,キーボードから1行分の文字列を入力できる。
        while( ! (aLine.equals( "" )) ) {
            System.out.println( "入力された文字列:" + aLine );
            aLine = scanner.nextLine( );
        }
        System.out.println( "入力は終了しました。" );
        scanner.close( );

    }

}

Scannerは,上例の使い方では RuntimeException のサブクラスの例外しか投げないので,
mainメソッドに throws節による例外指定をしなくてもエラーにならない。もちろん,
例外が投げられたらキャッチできないのでこのプログラムは異常終了することになる。
異常終了させたく無い場合は,しっかりと try-catch ブロックで投げられた例外をキャッチ
できるように対応すべきである。

以上。





















次に進む