2012/05/13

Javaの拡張for文でテキストファイルを1行ずつ読み込む方法

前回、「Javaでテキストファイルの読み込みを行う」をご紹介しました。
この方法で処理できる場合は良いですが、大きなテキストファイルの内容を1行ずつ読み込んで処理したい時や
CSVを1行ずつ読み込んで処理したい時があります。

テキストファイルを1行ずつ読み込む時は、一般的に BufferedReader.readLine() を使います。

しかし、この方法でプログラムを組むと、ソースが長くなり読みにくくなる人が多いと思います。
テキストファイルを読み込む処理と、テキスト内容を処理する部分をちゃんと分けて書けるとソースが読みやすくなりますが、
直感的に読みにくくなってしまうのがJavaの弱点ではないかと思います。

そこで、拡張for文を使って完結に、読みやすいソースをご紹介します。

Javaで次のようなコードを目指します!
(他の言語では、何もしなくてもこれで読めてしまうことが多いです。Pythonとか。)

File file = new File("sample.txt");
for (String line : file) {
 // テキストファイルの1行に対する処理を記述する
 System.out.println(line);
}
上記のように書けると便利だと思いませんか?



Javaでも同じようなソースコードを書くことが可能です。
拡張for文は、次の2つのインターフェースを実装するクラスを扱うことができます。
  • java.lang.Iterable
  • java.util.Iterator

ListやMapなどのコレクションを扱う際によく利用するインターフェースですね。
これを実装したファイル読み込みクラスを作ってしまえばいいのです。

では実際のソースコードをご紹介します。

■TextLineReader.java

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Iterator;

/**
 * {@link TextLineReader}クラスです。
 * 
 * @author ardor
 * @version 1.0
 */
public class TextLineReader implements Iterable<String> {
 
 /**
  * ファイルパス
  */
 private String filePath = null;
 
 /**
  * エンコーディング
  */
 private String encoding = null;
 
 /**
  * 指定したファイルをデフォルトエンコーディングで読む込むクラスを構築します。
  * 
  * @param filePath ファイルパス
  */
 public TextLineReader(String filePath) {
  this(filePath, null);
 }
 
 /**
  * 指定したファイルを指定エンコーディングで読み込むクラスを構築します。
  * 
  * @param filePath ファイルパス
  * @param encoding エンコーディング
  * @throws IOException ファイルが読み込めない場合
  */
 public TextLineReader(String filePath, String encoding) {
  // ファイルパス
  this.filePath = filePath;
  
  // エンコーディング
  this.encoding = encoding;
 }
 
 /**
  * テキストファイルの1行を読み込む{@link Iterator}を返します。
  * 
  * @return テキストファイル1行の反復子
  */
 @Override
 public Iterator<String> iterator() {
  try {
   return createIterator(filePath, encoding);
  }
  catch (IOException e) {
   throw new RuntimeException(e);
  }
 }
 
 /**
  * テキストファイルの1行を読み込む{@link Iterator}を返します。
  * 
  * @param filePath ファイルパス
  * @param encoding エンコーディング
  * @return テキストファイル1行の反復子
  * @throws IOException
  */
 protected Iterator<String> createIterator(String filePath, String encoding) throws IOException {
  Reader reader = null;
  if (encoding == null) {
   // 文字型入力ストリームクラス
   // デフォルトエンコーディングで読み込み
   reader = new FileReader(filePath); 
  }
  else {
   // 文字型入力ストリームクラス
   // エンコーディング指定で読み込み
   InputStream inputStream = new FileInputStream(filePath);
   reader = new InputStreamReader(inputStream, encoding);
  }
  return new TextLineBuffer(reader);
 }
 
 /**
  * {@link TextLineBuffer}クラスです。
  * テキストファイルを1行ずつ読み込みます。
  * 
  * @author ardor
  * @version 1.0
  */
 class TextLineBuffer implements Iterator<String>{
  
  /**
   * テキストファイル読み込みバッファ
   */
  private BufferedReader buff = null;
  
  /**
   * 文字型入力ストリームを指定してファイル読み込みクラスを構築します。
   * 
   * @param reader 文字型入力ストリーム
   */
  public TextLineBuffer(Reader reader) {
   buff = new BufferedReader(reader);
  }
  
  /**
   * ファイルが読み込み可能か返します。
   * 
   * @return true:読み込み可能、fa;se:読み込み不可能
   */
  @Override
  public boolean hasNext() {
   boolean hasNext = false;
   try {
    // 入力ストリームが読み込み可能か取得
    hasNext = buff.ready();
   } 
   catch (IOException e) {
    // 例外のため入力ストリームをクローズ
    close();
    throw new RuntimeException(e);
   }
   // 入力ストリームをクローズ
   if (!hasNext) {
    close();
   }
   return hasNext;
  }
  
  /**
   * ファイルの1行を読み込んで返します。
   * 
   * @return テキストファイルの1行
   */
  @Override
  public String next() {
   String next = null;
   try {
    // 1行読み込み
    next = buff.readLine();
   } 
   catch (IOException e) {
    // 例外のため入力ストリームをクローズ
    close();
    throw new RuntimeException(e);
   }
   // 入力ストリームをクローズ
   if (next == null) {
    close();
   }
   return next;
  }
  
  /**
   * テキストファイルの1行削除は行いません。
   * 未対応
   */
  @Override
  public void remove() {
   // removeする処理は実装しない
  }
  
  /**
   * 入力ストリームをクローズします。
   */
  private void close() {
   try {
    buff.close();
   }
   catch(Exception e) {
    // クローズ時の例外は何も行わない
   }
  }
 }
}

登場クラスは2つあります。
利用者が新しくnewするクラスは、TextLineReaderクラスです。
コンストラクタには、ファイルパス、エンコーディングを指定します。
これは前回ご紹介したクラスのメソッドでもお馴染みで、ファイルを読み込むための最低限の情報です。

また、TextLineReaderは、Iterableを実装します。
実装メソッドであるpublic Iterator iterator()では、ファイルを1行ずつ読み込んで返すクラスを返します。
ファイルを1行ずつ読み込んで返すクラスは、TextLineBufferというインナークラスです。
(Iteratorさえ実装してあるクラスならインナークラスでなくても可能です。)

Iteratorの実装メソッドは、hasNext()とnext()のみ実装しました。

それぞれ、BufferedReaderから取得できる値を返せばOKです。
また、読み込み終わったらちゃんとクローズ処理も行います。

途中で例外が発生することがあるかもしれません!
しかし、インターフェースの実装メソッドからは、例外をスローできないため、
RuntimeExceptionにラップしてスローします。
ここは必要に応じて、専用のRuntimeException派生例外クラスを作るといいかもしれませんね。
例えば、IORuntimeExceptionクラスとか。

以上で、拡張for文でテキストファイルを1行ずつ読み込みことができます。
使い方は次の通り。
  TextLineReader texts = new TextLineReader("sample.text", "UTF-8");
  for (String line : texts) {
   System.out.println(line);
  }

これで、ファイルを読み込んだ時の処理が綺麗に実装できると思います。
間違っている箇所等ありましたらコメントを頂けるとうれしいです。

0 件のコメント:

コメントを投稿