Javaで1行ずつテキストデータを読み込むイディオムの変遷
テキストデータを読み込む場合、言語にかかわらず1行ずつ読み込んで処理します。ここではこのイディオムを"read-lines"と呼ぶことにします。
スクリプト言語なんかだと大抵、最初からforeachで済んでしまっていたりしますが、初期のJavaではちょっと面倒な手順が必要でした。
新しいバージョンではだいぶ簡単簡潔に書けるようになりましたが、今度はたくさんありすぎて、どれがなんなのかが良くわからなくなっています(大袈裟)。
そこで今回は、"read-lines"の変遷についてまとめてみました。
最近Javaを始めた方がバージョンが古い時に書かれたソースコードを読むときに役に立つ...かも知れません。
目次
- 基本
BufferedReader
(Java1.1~)Scanner
と拡張for文 (Java5~)java.nio.file.Files
とtry-with-resources(Java7~)- Streamとラムダ式 (Java8~)
- 付録:標準入力の"read-lines"
※Java1.1より前については調べていません。
※本文中の標準APIのJavadocへのリンクは、すべてJava8のものにリンクしています。
基本
Javaでは、入力ソースを扱うクラスとして、主にjava.io.InputStream
とjava.io.Reader
という2つの基本クラスがあります。InputStream
はバイトストリームの入力、Reader
は文字ストリームの入力を行うクラスです。いずれも入力ストリームを表します。
以下の項では、小文字のアルファベットで書いたテキストファイルを読み込み、逐次その内容のアルファベットを大文字に変換する処理を書きます。
- 入力データ(
files.txt
)
aaa bbb ccc
- 実行結果の例
AAA BBB CCC
BufferedReader
(Java1.1~)
Java5より前のバージョンでは、read-linesする一般的な方法はjava.io.BufferedReader
で入力文字ストリームをラップするものでした。
機能的には十分なんですけれど、簡潔さに欠けますね。
ちなみに、対応するReader
がない場合は、InputStreamReader
でラップする必要があります。
- 例:
BufferedReader
を使ったread-lines
// import java.io.*; try { BufferedReader br = new BufferedReader(new FileReader("file.txt")); try { while (true) { String line = br.readLine(); if (line == null) { break; } System.out.println(line.toUpperCase()); } } finally { br.close(); } } catch (IOException e) { e.printStackTrace(); }
Scanner
と拡張for文 (Java5~)
Java5では、java.util.Scanner
という、よりread-linesをしやすいクラスが追加されました。
Scanner
には、複数の入力ソースに対応したコンストラクターが用意されています。
String
が引数のコンストラクターもあります。FileInputStream
などのクラスから類推すると、文字列はファイルパスかと思ってしまい紛らわしいのですが、Scanner
の場合、文字列はそれ自体が入力ソースです。
- 例:
Scanner
を使ったread-lines
// import java.io.*; // import java.util.Scanner; Scanner scanner = new Scanner(new File("file.txt")); // throws java.io.FileNotFoundException try { while (scanner.hasNextLine()) { String line = scanner.nextLine(); System.out.println(line.toUpperCase()); } } finally { scanner.close(); } // 文字列自体を入力にする Scanner scanner2 = new Scanner("aaa\nbbb\nccc"); // 以下略
Scanner
はIterator
インターフェイスを実装しているので、イテレーターとして使うこともできます。
- 例:
Scanner
をイテレーターとして使ってread-linesする
// import java.util.*; // Iterator, Scanner Scanner scanner = new Scanner("aaa\nbbb\nccc"); try { Iterator<String> it = scanner; // わざわざ代入しなくても良い while (it.hasNext()) { String line = it.next(); System.out.println(line.toUpperCase()); } } finally { scanner.close(); }
でも、これだと直接使うのと変わらないのであまり面白くありません。
どうせなら(?)直接、拡張for文で使ってみたいですね。
ただそれにはIterable
インターフェイスを実装している必要があります。
あまり得することはありませんが、いくつかの例を書いておきます。
- 例:
Scanner
をIterable
に変換して拡張for文を使う
// import java.util.*; // Iterator, Scanner Scanner scanner = new Scanner("aaa\nbbb\nccc"); try { final Iterator<String> it = scanner; // 外側のを直接finalにしてもOK Iterable<String> iterable = new Iterable<String>() { // @Override // ※インターフェイスのメソッドに@OverrideをつけられるのはJava6から public Iterator<String> iterator() { return it; } }; for (String line : iterable) { System.out.println(line.toUpperCase()); } } finally { scanner.close(); }
Apache Commons Collections4(バージョン4)のIteratorUtils
というクラスを使うともっと簡潔に書けます。
- 例:Commons Collections4の
IteratorUtils.asIterable
を使ってScanner
をIterable
に変換
// import java.util.Scanner; // import org.apache.commons.collections4.IteratorUtils; Scanner scanner = new Scanner(new File("file.txt")); // throws java.io.FileNotFoundException try { for (String line : IteratorUtils.asIterable(scanner)) { System.out.println(line.toUpperCase()); } } finally { scanner.close(); }
java.nio.file.Files
とtry-with-resources(Java7~)
Java7では、New I/O 2の機能のひとつとして、java.nio.file.Files
クラスが追加されました。このクラスはユーティリティークラスです。
Java7では他にも、言語機能にtry-with-resourcesが追加されています。これは、直接read-linesとは関係ありませんが、read-linesのコードが少し簡潔に書けるようになります。特に、コンストラクターでは無くファクトリーからインスタンスを生成する場合は、if (in != null) in.close;
のようなコードを書かなくてはいけなくなりますので、try-with-resourcesがあればそれを考えなくても良くなります。
- 例:
Files.newBufferedReader
とtry-with-resourcesでread-lines
// import java.io.*; // import java.nio.file.*; // (追記)Java7の時点ではCharsetは必須 Java8では第2引数を省略してUTF_8をデフォルトとするオーバーロードメソッドが追加された try (BufferedReader br = Files.newBufferedReader(Paths.get("file.txt"), StandardCharsets.UTF_8)) { // 自動close brがnullでもOK while (true) { String line = br.readLine(); if (line == null) { break; } System.out.println(line.toUpperCase()); } } catch (IOException e) { e.printStackTrace(); }
Files
クラスには他にも、1行ずつ読み込むread-linesとは違いますが、全ての行をList
として返すFiles.readAllLines
というメソッドもあります。
ただし、一度にすべてのデータをArrayList
に読み込んでしまうので、大きなサイズのファイルの読み込みには向きません。ドキュメントにもそう書いてあります。
Streamとラムダ式+関数型インターフェイス (Java8~)
Java8では、Stream APIと言語機能としてラムダ式と関数型インターフェイスが追加されました。
Streamはread-linesにぴったりの機能なわけですが、ラムダ式+関数型インターフェイスと組み合わせることで真価を発揮します。
Files
クラスには、Streamに対応したFiles.lines
メソッドが追加されています。また、同類の機能としてBufferedReader#lines
メソッドが追加されています。
- 例:
Files.lines
でread-lines
// import java.io.IOException; // import java.util.stream.Stream; // import java.nio.file.*; // Files, Paths; try (Stream<String> a = Files.lines(Paths.get("file.txt"))) { // 自動close a.map(String::toUpperCase).forEach(System.out::println); } catch (IOException e) { e.printStackTrace(); }
付録:標準入力の"read-lines"
標準入力は"read-lines"を使って読み取るケースが多いです。そのため、ここまで書いてきた方法をそのまま使うことはできますが、ひとつ気を付けなければいけないことがあります。
それは、「クローズしてはいけない」ということです。
標準入力からの読み込みは、System.in
(java.lang.System
クラスのstaticフィールド)をInputStream
として
BufferedReader
を使って書くと、こうなります。
// import java.io.*; try { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try { while (true) { String line = br.readLine(); // 入力せずENTER if (line == null || line.length() == 0) { break; } System.out.println(line.toUpperCase()); } } finally { br.close(); } System.in.read(); // => java.io.IOException: Stream closed } catch (IOException e) { e.printStackTrace(); }
BufferedReader#close
から間接的にSystem.in
のストリームを閉じてしまうと、次からは標準入力を読み込むことができなくなってしまいます。
回避方法のうちひとつは、単にクローズしないようにするだけですが、同じPrintStream
なのにクローズするかしないかを選択しなければいけないというのは、どうも腑に落ちません。
それに、コンパイラースイッチのpotential resource leakに引っかかってしまうのでちょっと鬱陶しいのです。
別の方法のひとつは、Apache Commons IOのCloseShieldInputStream
を使うものです。
System.in
を更にCloseShieldInputStream
でラップすれば、外側でcloseした場合でもいちばん内側のSystem.in
までcloseが伝播しません。
- 例:
CloseShieldInputStream
でSystem.in
がcloseされるのを防ぐ
// import java.io.*; // import org.apache.commons.io.input.CloseShieldInputStream; try { BufferedReader br = new BufferedReader(new InputStreamReader(new CloseShieldInputStream(System.in))); try { while (true) { String line = br.readLine(); // 入力せずENTER if (line == null || line.length() == 0) { break; } System.out.println(line.toUpperCase()); } } finally { br.close(); } System.in.read(); // 入力せずENTER => OK } catch (IOException e) { e.printStackTrace(); }
Scanner
の場合も同様に使えます。
このアイディアは、下記のページで知りました。
(おわり)