« ◇怪しい仮説vs怪しい反論:水棲人類説をめぐって | トップページ | ♪コントラバス・プチ・メロディー:Baseだってメロディーを »

Javaでファイルオープン:文字コードや追加モードなど

メモです。

Java(の標準のライブラリ)はファイルオープンが極めて煩わしい ものとなっています。

しかしながらファイルのオープンを嫌うと、システム構成や デバッグ法などが硬直したものとなり、大きな損失 を生みだす元となります。

ファイルオープンの心理的壁を乗り越えるため、 次の観点でまとめます。コピペで動きます。

  • テキストかバイナリか
  • 読み込みか書き出しか
  • 書き出しの場合追加(APPEND)か否か
  • テキストの場合文字コードセットは何か
文字コードセットは決して無視してはならない重要な指定です。

さらに、ファイル生成時にフォルダも生成する形も載せます。

クラスは全てjava.ioパッケージのものですので、

import java.io.*;
でインポートし使用できます。もちろん*を使わず一個一個書いても構いません。

Javaには解体子がありませんので、資源の後始末(close)はクラスの利用者が 明示的に行わなければなりません。何もしなくてもガベッジコレクト時に close処理は行われますが、それまでに資源を食いつぶす可能性があります ので、必ずcloseを発行すべきです。
ここで使うクラスのcloseは最外郭のクラスにのみ 発行すればよいようになっています。(BufferedReaderのcloseを 呼べばInputStreamReaderのcloseは行われます)

なお、以下のオープン/クローズ手続きのコードはこの記事から 試験プログラムに コピー&ペーストして動作確認を行ってあります。

補足:2013/3/20
UTF-8でBOMがある場合Javaの標準ではうまく読み込めません。 これに対処する方法を「JavaでUTF-8のBOMに対処する」に載せました。

 テキスト、読み込み(BufferedReader)

テキストファイルの読み込みのオープンは次のように行います。

   try{
      String         fileName = "test.txt"; // ファイル名
      String         charSet  = "utf-8";    // 文字コードセット
      BufferedReader br= new BufferedReader(
                        new InputStreamReader(
                           new FileInputStream(
                              new File(fileName))
                          ,charSet));// 省略するとシステム標準
      //...
      br.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }

System.inは歴史的理由によりBufferedReaderではなくInputStreamです。SUNはテキスト入力 用としてはInputStreamは避けるべきであるとしています。

文字コードとしてシステム毎の標準を使用したい場合はcharSet引数を省略します。ただし 混乱の元となりますので推奨できません。

構築手続きを簡略化したFileReaderというクラスが用意されていますが 文字コードセット指定ができず混乱の 元となるので絶対に使用すべきではありません。企業などでは コーディング規約にFileReader使用禁止を明示すべきです。

補足:読み込み
1行単位での読み込みを次のように行うことができます。

   String line;
   while( (line=br.readLine())!=null ){
      // lineに改行コードを含まない1行分の文字列が入っている。
      }
   //..
   br.close(); // 忘れないよう:Javaには解体子がない!

 テキスト、書き出し(PrintWriter)

テキストファイルの書き出しのオープンは次のように行います。

   try{
      String       fileName   = "test_out.txt"; // ファイル名
      String       charSet    = "utf-8";        // 文字コードセット
      boolean      append     = false;          // 追加モード
      boolean      auto_flush = true;           // 自動フラッシュ
      PrintWriter  pw = new PrintWriter(
                           new BufferedWriter(
                              new OutputStreamWriter(
                                  new FileOutputStream(
                                     new File(fileName)
                                    ,append)
                                 ,charSet)) // 省略するとシステム標準
                          ,auto_flush);
      //...
      pw.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }

System.outは歴史的理由によりPrintWriterではなくPrintStreamです。SUNはテキスト出力 用としてはPrintStreamは避けるべきであるとしています。

文字コードとしてシステム毎の標準を使用したい場合はcharSet引数を省略します。ただし 混乱の元となりますので推奨できません。

構築手続きを簡略化したFileWriterというクラスが用意されていますが 文字コードセット指定ができず混乱の 元となるので絶対に使用すべきではありません。企業などでは コーディング規約にFileWriter使用禁止を明示すべきです

自動フラッシュはprintlnなどでflushを行うものですが、PrintStreamと 異なり出力する文字が改行含むことによるflushは行いません。
特に効率化のためなどの意図を持たない場合自動フラッシュにしておく 方が安全です。

補足:出力
println()やprintf()で出力できます。

   pw.println("試験出力");
   ...
   pw.close(); // 忘れないよう:Javaには解体子がない!

 テキスト、書き出し(PrintWriter):フォルダ生成

テキストファイル書き出しのオープンでフォルダが無い場合フォルダも生成する形は次にようになります。

   try{
      String       fileName   = "f1/f2/f3/test_out.txt"; // ファイル名
      String       charSet    = "utf-8";        // 文字コードセット
      boolean      append     = false;          // 追加モード
      boolean      auto_flush = true;           // 自動フラッシュ
      File         fi         = new File(fileName);
      File         dir        = fi.getParentFile();   
      if ( dir!=null && !dir.exists() ) dir.mkdirs(); // フォルダが無い場合生成
      PrintWriter  pw = new PrintWriter(
                           new BufferedWriter(
                              new OutputStreamWriter(
                                  new FileOutputStream(
                                     fi
                                    ,append)
                                 ,charSet)) // 省略するとシステム標準
                          ,auto_flush);
      //...
      pw.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }
階層化されたフォルダも生成されます。
dirのnullチェックは必須ですので、忘れないようにしてください。

こんな面倒な手続きが必要だとは、Javaはまっとうではありません。

 バイナリ、読み込み(BufferedInputStream)

バイナリファイルの読み込みのオープンは次のように行います。

   try{
      String              fileName = "test.dat"; // ファイル名
      BufferedInputStream bis = new BufferedInputStream(
                                   new FileInputStream(
                                      new File(fileName)));
      //...
      bis.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }

補足;入力
入力はread()関数で行うことができます。

   byte[] buf= new byte[4];
   int offset= 0;
   int len;
   while( (len=bis.read(buf,offset,buf.length))!=-1 ){ // 読み込み
      //...
      }
   //
   bis.close(); // 忘れないよう:Javaには解体子がない!

 バイナリ、書き出し(BufferedOutputStream)

バイナリファイルの書き出しのオープンは次のように行います。

   try{
      String               fileName = "test_out.dat"; // ファイル名
      boolean              append   = false;          // 追加モード
      BufferedOutputStream bos = new BufferedOutputStream(
                                    new FileOutputStream(
                                       new File(fileName)
                                      ,append));
      //...
      bos.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }

補足;出力
入力はwrite()関数で行うことができます。

   byte[] buf= { 1,2,3,4 };
   int offset= 0;
   int len   = 4;
   bos.write(buf,offset,len); // 出力
   //
   bos.close(); // 忘れないよう:Javaには解体子がない!

 バイナリ、書き出し(BufferedOutputStream):フォルダ生成

バイナリファイルの書き出しのオープンでフォルダが無い場合フォルダも生成する形は次にようになります。

   try{
      String               fileName = "f1/f2/f3/test_out.dat"; // ファイル名
      boolean              append   = false;          // 追加モード
      File                 fi       = new File(fileName);
      File                 dir      = fi.getParentFile();   
      if ( dir!=null && !dir.exists() ) dir.mkdirs(); // フォルダが無い場合生成
      BufferedOutputStream bos = new BufferedOutputStream(
                                    new FileOutputStream(
                                       fi
                                      ,append));
      //...
      bos.close();
      }
   catch(Exception e){
      e.printStackTrace(System.err);
      // .. 例外処理
      }
階層化されたフォルダも生成されます。
dirのnullチェックは必須ですので、忘れないようにしてください。

こんな面倒な手続きが必要だとは、Javaはまっとうではありません。

 注意!やってはならないコーディング

ファイルのオープン法の説明には使用クラスを明示するため次のような コードを示しているものがあります。

   File               fi = new File("testout.txt");             //やってはならない
   FileOutputStream   fos= new FileOutputStream(fi,false);      //やってはならない
   OutputStreamWriter osw= new OutputStreamWriter(fos,"utf-8"); //やってはならない
   BufferedWriter     bw = new BufferedWriter(osw);             //やってはならない
   PrintWriter        pw = new PrintWriter(bw,true);            //やってはならない
これは実際にはやってならないコーディングです。

途中のクラスのオブジェクトは単純なワークではありません。それらに アクセスすると副作用をもつ危険なものなのです。

中間においたワークに対してアクセスを行うと期待の動作は得られません。

   bw.print("AAA:");  // バッファリングされる
   osw.write("BBB:"); // NG!! バッファリングされず出力される
   bw.print("CCC:");  // バッファリングされる
   bw.close();        // ファイル内容はBBB:AAA:CCC:となる
これは単にoswにアクセスしたバグということではなく、 そもそも触ってはならないオブジェクトoswを名前付きで生成すべきではなかったのです。

次のような場合ワークをいじってもなんの問題もありません。

   String              fileName = "test.dat"; // ファイル名
   BufferedInputStream bis = new BufferedInputStream(
                                new FileInputStream(
                                   new File(fileName)));
   fileName="test2.dat"; // 何の問題もない
ワークとしてのオブジェクト生成は「可能な限り」このようなものに限るべき なのです。

 補足:エラー時の中間オブジェクトの資源解放

Javaは解体子を持たずメモリ以外の資源の 管理が完全に利用者の責任とされるにも関わらず無名の中間インスタンスを生成 できる、本質的に間違った愚かな言語仕様を持っています。

無名のインスタンスを作った場合資源の例外発生時の解放がただちにはできない 場合が起こりえます。
本記事の例では例えば文字コードが不正な場合、FileOutputStreamの資源が ただちには解放されません(やがて解放されるので現実的にこれが問題に なることはありません;初期のJavaでは動作不能となる場合がありました)。

必ずしも必要ではありませんし、コードを汚くするので良い方法とは言えません が、どうしても気になるなら次の形をとってください。

   String       fileName       = "test_out.txt"; // ファイル名
   String       charSet        = "utf-8";        // 文字コードセット
   boolean      append         = false;          // 追加モード
   boolean      auto_flush     = true;           // 自動フラッシュ
   boolean      delete_on_error= true;           // エラー時に空ファイルを残さない
   PrintWriter  pw;// ※
   /* */{
      FileOutputStream   fos= new FileOutputStream(
                                     new File(fileName),append);
      try{
         pw=new PrintWriter(
                new BufferedWriter(
                    new OutputStreamWriter(
                          fos
                         ,charSet))
                ,auto_flush );
         }
      catch(Exception e){
         fos.close();
         if( delete_on_error ){
             (new File(fileName)).delete();
             }
         throw e;
         }
      }
   pw.println("test-test");// ※2
// ※:pwは初期化すべきではありません。このプログラムでは、pwを使用する※2部分で
//    は確実にpwは初期化されています。pwの初期化が失敗するルートはcatchブロック
//    で終わり、※2には到達しません。
//    ただし、解析能力の低いコンパイラでは「pwが初期化されていない」というエラー
//    となる可能性はあります。その場合のみnullで初期化するか、可能であればそのコ
//    ンパイラの使用を停止しより高い解析能力を持つコンパイラに切り替えます。
null初期化した中間オブジェクトをずらずら並べてそれら全てをfinallyブロックで if(XX!=null)XX.close()するといった書き方は行うべきではありません。
う~ん、それにしても単にファイルをオープンするってだけでこれほど 大がかりなコードが必要となるのはまっとうではない。

 同じことを何度も繰り返し書くか関数にまとめるか

Javaはあちらこちらに殆ど同じコードを何度も 繰り返し書く(コピペする)ことが多いのですが、 コピペの世代を経るにつれ形が崩れ思わぬバグを大量にあちらこちら に埋め込むこととなります。
もし、同じコードを繰り返しコピペするにしても、元ネタはできるだけ 1か所にすべきです。
また、引数に直接trueやfalseを書くことは意味が分かりづらいため避けるべきです。

本来ならこれらの単純な、しかし煩わしい手続きは関数としてまとめられる べきです。

 Javaの解体子に関するメモ

Javaにはスコープから消える時に呼ばれる解体子はありませんが、ガベッジコレクト時に呼ばれる関数はあります。

finalize関数です。次のような形で定義できます。

   class X {
      //...
      @Override
      protected void finalize() throws Throwable {
         // 解放処理;
         }
      }
ただ、この関数が呼ばれるタイミングは利用者が制御できませんので、使わない方がよいと考えます。

 ###

この記事は2009年1月にブログ「酔歩惑星」に載せたメモ記事を少し整理 したものです。(元の記事はファイルオープン部だけをまとめる形には なっていませんでした)

### 2010/4/22
コピペによる動作確認済であることの記述追加。
入出力法の補足を追加。

### 2010/4/26
importに関する記述追加。
closeを最外殻にのみ発行すればよいという記述追加。

### 2010/5/10
やってはならない記述追加。

### 2010/6/10
例コードで中でのpwの初期化に関する注意文追加

### 2010/10/6
サンプルコードをtry-catchで囲むようにした。異常処理は運用に応じ 違うのでここに書くのは避けたくはあったが、Exceptionが発生するの だということを示しておいた方がよいと判断した。Exceptionを catch仕分けることは無意味な複雑化を呼ぶため行わない。
Javaは解体子のない極めて貧弱な言語のためメモリ以外の資源は全て アプリケーション側で明示的に後始末をしないといけないのでfinally を使った後始末をする必要がある場合もあるが、ここには載せない。

### 2011/12/10
フォルダも生成する形を追加

 単純手続きをまとめたライブラリのご紹介

ここで述べたファイルオープン機能などをまとめたライブラリが公開 されています。単純なものとはいえ、ドキュメントも整備され、 十分な試験も行われたものです。
エラー時の中間オブジェクトのclose()なども行ってあります。

例えばテキストファイルの書き出し用オープンは次のような形で行います。

   PrintWriter pw  = hiU.openTextFileW("test.txt","utf-8",hiU.APPEND);
簡単である上、視認性が極めて高いと考えています。

(次期リリース)1.23版からはフォルダの生成機能も含むようになります。

ライブラリ説明

ダウンロードページも含むライブラリ全体の説明は にあります。
オツアンドサンズのホームページは です。

もちろんファイルオープンだけではなく、 データダンプ機能、関数ネストを表示するトレース機構、 コマンド引数解析など単純でありながら手間がかかるものなどを 簡単にする機能他、多くの実用機能セットとなっています。

|

« ◇怪しい仮説vs怪しい反論:水棲人類説をめぐって | トップページ | ♪コントラバス・プチ・メロディー:Baseだってメロディーを »

トラックバック


この記事へのトラックバック一覧です: Javaでファイルオープン:文字コードや追加モードなど:

« ◇怪しい仮説vs怪しい反論:水棲人類説をめぐって | トップページ | ♪コントラバス・プチ・メロディー:Baseだってメロディーを »