Deep Side of Java〜Java 言語再入門 第2回 〜 Java 文法を中心に

組み込みクラスライブラリ・パッケージ

java.io.*






組み込みクラスライブラリ・パッケージ

java.io.*

Java の入出力は抽象クラスを多重に使って実現されている。なので、一種のイディオムとして理解するのが良かろう。ここでは、ケース別に分けて理解することにする。

テキスト入出力

普通にファイルから行指向のテキストの Read/Write をするには次のようにする。入出力は JDK のバージョンによってかなり大きな変更があったが、現在使われるモデルは Reader/Writer モデルである。特に入出力は、バッファリングを行って効率の良い BufferedReader/Writer クラスを使うべきである。この Reader/Writer はそのプラットフォームでデフォルトである日本語コーディング(UNIX=EUC, Win=SJIS)の文字列を正しく扱う。

try {
        String line;
        BufferedReader br = new BufferedReader( 
                              new FileReader( "infile.dat" ) );
        BufferedWriter bw = new BufferedWriter( 
                              new FileWriter( "outfile.dat" ) );
        while( (line = br.readLine()) != null ) {
                bw.write( line );
                bw.newLine();
        }
        bw.flush();
        bw.close();
        br.close();
} catch( IOException e ) {

このプログラムは次の処理をすることになる。

  1. 改行文字については、DOS流(0x0d0x0a)・MAC流(0x0d)・UNIX流(0x0a)のいずれも正しく改行文字として判定して1行を読み込み、そのプラットフォームで利用される改行文字を1行の末尾に付加する。要するに改行文字はそのプラットフォームに合わせて変換する。

  2. 日本語コーディングについては、そのプラットフォームで標準として採用されているコーディングで読み込み、標準として採用されているコーディングで書き出す。標準外のコーディングは読み込みのときに判定せずに、異常な文字(SJIS)あるいは単なるASCII(JIS)として扱う。

標準外のエンコーディングを扱う場合には明示的なプログラミングが必要である。

バイナリデータ入出力

次はバイナリデータの入出力である。例えば既存のデータフォーマットのファイルを読み込むのに使うことができる。たとえば、次のようなC言語構造体によって書き込まれたデータがあることにしよう。

struct Output {
        int x;       /* Little Endian の 4byte とする */ 
        char s[32];  /* SJIS でコーディングされた日本語とする */
};

これを読み込み内容を表示するプログラムは次の通り。

import java.io.*;

public class BinR {
     int x;
     String s;

     public static void main( String [] args ) {
          new BinR();
     }

     BinR() {
          try {
               DataInputStream dis = new DataInputStream( 
                            new FileInputStream( "datafile.dat" ) );
               while( readStruct( dis ) ) {
                    System.out.println( "x=" + x + " s=" + s );
               }
               dis.close();
          } catch( IOException e ) {
               System.err.println( e );
          }
     }

     boolean readStruct( DataInputStream dis ) {
          byte [] rx = new byte [4];
          byte [] rs = new byte [32];

          try {
               /* read メソッドは、配列サイズ分だけ byte を読み込む */
               /* read メソッドは EOF では例外を投げない */
               if( dis.read( rx ) <= 0 ) return false;
               if( dis.read( rs ) <= 0 ) return false;

               /* Endian の変換(Java は Big Endian) */
               x = (int)rx[0] & 0x000000ff;
               x |= (((int)rx[1] << 8) & 0x0000ff00);
               x |= (((int)rx[2] << 16) & 0x00ff0000);
               x |= (((int)rx[3] << 24) & 0xff000000);

               /* SJIS -> UNICODE 変換は String コンストラクタ
                      でできる */
               s = new String( rs, "SJIS" );
          } catch( Exception e ) {
               return false;
          }
          return true;
     }
}

オブジェクト入出力(シリアライゼーション)

構造体(レコード型)は、ファイルフォーマットを読み書きする手段として使われてきた。しかし、Java には「構造体はない」。バイナリデータの入出力として扱うのは繁雑に過ぎるために、Java は面白い解決方法を提示している。これが「シリアライゼーション」である。つまり、Java のオブジェクトをそのままファイルなどに入出力するという仕様である。だから、次のプログラムでは、自分で定義したクラスを入出力しているが、当然配列などもオブジェクトなので、この要領で入出力できる。

次のクラスが保存されるべき構造体に相当するクラスである。このクラスでは Serializable インターフェイスを implements している。この interface は、シリアライズのための準備ができていることをマークするために使われるインターフェイスであり、特にメソッドなどを実装するようには要求されていない。

import java.io.*;

class Target implements Serializable {  /* 保存対象のクラス */
     int x;    /* 保存対象 */
     String s; /* 保存対象(クラス) */
     double d; /* 保存対象 */
     Target( String s ) {
          if( s.equals( "first" ) ) {
               this.x = 1; this.s = s; this.d = 1.1111;
          } else if( s.equals( "second" ) ) {
               /* などなど、引数によって適当にデータをセット */
          }
     }
     public String toString( ) {
          return "Target: x=" + x + " s=" + s + " d=" + d;
     }
}

もし、変数フィールドが transient で修飾されていると、その内容はシリアライズされない、言い換えれば保存されない。これは変数によっては保存することに意味がない、一時的な情報が入っている変数を定義する場合もあるから、こういう言語の上でのサポートがあるのである。

では、Target クラスを読み書きする側のプログラムはこうである。

public class Seri {
     public static void main( String [] args ) {
          if( args.length < 3 ) {
               System.err.println( "java Seri sym [-in | -out] outfile" );
               System.exit( 1 );
          }
          new Seri( new Target(args[0]), args[1], args[2] );
     }

     private Seri( Target t, String flag, String file ) {
          if( flag.equals( "-in" ) ){
               t = input( t, file );
          } else if( flag.equals( "-out" ) ){
               output( t, file );
          }
          System.out.println( t );
     }
                     
     private Target input( Target t, String file ) {
          try {
               ObjectInputStream ois = new ObjectInputStream( 
                                          new FileInputStream(file) );
               t = (Target)ois.readObject();
               ois.close();
          } catch( Exception e ) {
               System.err.println( e );
          }
          return t;
     }

     private void output( Target t, String file ) {
          try {
               ObjectOutputStream oos = new ObjectOutputStream( 
                                           new FileOutputStream(file) );
               oos.writeObject( t );
               oos.close();
          } catch( IOException e ) {
               System.err.println( e );
          }
     }
}

この保存されたオブジェクトのファイルをダンプしてみよう。

0000:AC ED 00 05 73 72 00 06 54 61 72 67 65 74 A1 65        ....sr..Target.e
0001:D0 EF 16 60 E0 C9 02 00 03 44 00 01 64 49 00 01        ...`.....D..dI..
0002:78 4C 00 01 73 74 00 12 4C 6A 61 76 61 2F 6C 61        xL..st..Ljava/la
0003:6E 67 2F 53 74 72 69 6E 67 3B 78 70 3F F1 C7 10        ng/String;xp?...
0004:CB 29 5E 9E 00 00 00 01 74 00 05 66 69 72 73 74        .)^.....t..first

単に構造体のようにデータだけが保存されているのではなく、保存クラス名(バージョンもいれることができる)や、そのクラスのメンバである java.lang.String 型などの情報も入っていることがわかる。

しかし、このオブジェクトシリアライゼーションの技術は、単に構造体の代わりにオブジェクトを保存するためにあるのではない。それよりも RMI のような分散コンピューティング環境を実現したり、あるいは Java Bean のような統一的な部品定義のもとに動作する機能を実現するのにも使われている。だから、このオブジェクトシリアライゼーションの応用は広いわけで、その基礎としてこれを理解されたい。

java.io パッケージの階層構造

これで見てきたように、java.io.* のクラスには使い方に定型がある。そのモデルは大体次の通り(Reader/Writer モデルは別だが)。

  1. まず開くべきファイル名(String型)がある。

  2. これを、現実的なファイルを表すクラス java.io.File のインスタンスをファイル名から作る。

  3. File クラスのオブジェクトからストリームを作成する。これが実質上のオープン処理である。これは (Input|Output)Stream クラスが担当するが、このクラスは典型的な抽象クラスである。なので、実装クラスは File を開く File(Input|Output)Stream クラスになる。

  4. そのストリームに対して入出力をするのだが、「どういう入出力をするのか」を定めるインターフェイスが定まっている。これを担当するのが DataInput, ObjectOutput などのインターフェイスであり、「何からどうやって入出力をするのか」をインターフェイスによって実装した、実際の入出力クラスが定義される。つまり、DataInputStream や ObjectOutputStream が、実際の入出力を担当するのである。これはいわゆる「Decorator」デザインパターンによる実現である。

ということは、ディスク上のファイルに対して読み書きをするストリームが File(Input|Output)Stream であるとすれば、たとえば、標準入出力は InputStream と PrintStream クラスで宣言されているので、これをやはり、同じスキームで利用することができる。具体的には標準入力から読み込むならば、

BufferedReader br = new BufferedReader( 
                       new InputStreamReader( System.in ) );

で Reader を作って使うとテキスト入出力になる。ということは、普通ファイルから Reader を作るときのコードは、次のものと同じである。

Reader fr = new FileReader( "input.txt" );
Reader ir = new InputStreamReader( new FileInputStream( 
                                          new File( "input.txt" ) ) );

また、もしリダイレクトでオブジェクトを読むのならば、

ObjectInputStream = new ObjectInputStream( System.in );

であるし、文字列をあたかもファイル入出力であるかのように扱う StringReader/StringWriter の Reader/Writer が用意されている。また、それらにさまざまな修飾をする BufferedInputStream(バッファリングをつけ加えて入出力を効率化する), java.util.zip.DeflaterOutputStream, java.util.zip.InflaterInputStream(圧縮形式のR/W) なども、FilterInputStream を基底クラスとして設計して、同じ Stream としてパイプ処理のように繋ぎ合わせて利用することができる。このように抽象化の層が合理的に設計されているので、よく理解されたい。これをデザインパターン用語で「Decorator」と呼ぶ。

InputStreamReader クラスでは、コンストラクタで入力文字コーディングを指定できるが、この中に「JISAutoDetect」というコーディングを自動判定する機能がある。これを使って、どんなコーディングのファイルも適切に出力する cat 風プログラムを、この java.io.* パッケージの理解の仕上げとして書いてみよう。当然引数がない場合には、標準入力から読む。

import java.io.*;

public class EveryCodingCat {
     public static void main( String [] args ) {
          new EveryCodingCat( args );
     }

     private EveryCodingCat( String [] filenam ) {
          InputStream is;
          if( filenam.length == 0 ) {
               is = System.in;
               try {
                    readProc( is );
               } catch( IOException e ) {
                    System.err.println( "ERROR: at (stdin)" );
                    System.err.println( e );
                    System.exit( 1 );
               }
          } else {
               for( int i = 0; i < filenam.length; i++ ) {
                    try {
                         is = new FileInputStream( filenam[i] );
                         readProc( is );
                         is.close();
                    } catch( IOException e ) {
                         System.err.println( "ERROR: at " + filenam[i] );
                         System.err.println( e );
                         System.exit( 1 );
                    }
               }
          }
     }

     private void readProc( InputStream is ) throws IOException {
          String line;
          BufferedReader ir = new BufferedReader( 
                     new InputStreamReader( is, "JISAutoDetect" ) );
          while( (line = ir.readLine()) != null ) {
               System.out.println( line );
          }
     }
}

java.io.File

では、File クラスについて補足しよう。File クラスは実際のファイルを抽象化したオブジェクトである。だから、通常のファイルに対して要求されるさまざまな操作が実装されており、しかもプラットフォームに依存しないように考えられている。

boolean exists()
そのファイルが存在するかどうかを返す。
boolean isDirectory()
boolean isFile()
そのパスがディレクトリ or 実ファイルであるかどうかを判定する。
boolean canRead()
boolean canWrite()
そのパスが読み書きできるかどうかを判定する。
boolean delete()
そのファイルを削除する。
boolean mkdir()
そのディレクトリ名のディレクトリを作成する。
int length()
そのファイルの内容サイズを返す。
String [] list()
そのディレクトリにあるすべてのファイル名を返す。

ファイル追加書き込みとランダムアクセス

現実的なプログラマとしては気になるところである、ファイル追加書き込みの仕方とランダムアクセスの手法について述べる。

ファイルを追加書き込みする(既存ファイルが出力対象として指定された時に、ファイル内容を抹消せずに、末尾に追加する)ためには、FileWriterクラス(テキスト出力の場合)、FileOutputStream(バイナリ出力の場合)のコンストラクタの第二引数が、append フラグになっている。つまり次のようにすれば追加書き込みになる。

BufferedWriter bw = new BufferedWriter( new FileWriter( "data.txt", true ) );

ランダムアクセスはやや異例で、FileInput(Output)Stream の機能と、DataInput(Output)Stream の機能とが合体した、特別なクラスである RandomAccessFile クラスを使う。だから、このクラスはファイル名(String 型でも File 型でも)を指定してコンストラクタを呼び出し、このクラスのメソッドとして DataInput(Output) の interface を備え、readInt(), writeDouble() などのメソッドを備えると共に、lseek(2) や ftell(2) に相当するメソッドを備えているのである。

long getFilePointer()
現在のファイルポインタの位置を返す。要するに ftell(2) の機能である。
void seek(long pos)
現在のファイルポインタの位置を先頭から pos byte目にセットする。要するに lseek(2) の機能である。

使い方の実例は次の通り。1レコードは int値 3個で出来ている(12byte)としてみよう。起動第1引数はデータファイル名、第2引数はnレコード目のデータを表示するとする(データはビッグエンディアンにしておく)。

import java.io.*;

public class RandomAccessTest {
    int dataNo;
    public static void main( String [] args ) {
        if( args.length != 2 ) { System.exit(1); }
        try { dataNo  = Integer.parseInt( args[1] ); }
        catch( NumberFormatException e ) { System.exit(1); }
        new RandomAccessTest( args[0] );
    }
    RandomAccessTest( String file ) {
        int [] data = new int [3];
        RandomAccessFile raf;
        /* データサイズを掛けてオフセットに変換 */
        long offset = (long)dataNo * 12L; 
        try {
            raf = new RandomAccessFile( file, "r" );
              /* 第二引数は読み込み("r")、読み書き("rw")のフラグ */
            raf.seek( offset );
            for( int i = 0; i < 3; i++ ) {
                data[i] = raf.readInt();
            }
            System.out.println( "1:" + data[0] + " 2:" + data[1] 
                              + " 3:" + data[2] );
        } catch( IOException e ) {
            e.printStackTrace();
        }
        raf.close();
    }
}



copyright by K.Sugiura, 1996-2006