1. はじめに
まず、FileReader と FileWriter がどう動くかを思い出しましょう。これらで read() や write() を呼ぶたびにファイルシステムへのアクセスが発生し、実際にディスクから1文字を読み取ったり1文字を書き込んだりします。ファイルが大きいのに1文字ずつ読み書きすると、数千回、あるいは数百万回ものディスクアクセスというマラソンになってしまいます。そしてディスクは、残念ながらそれほど速いわけではありません。
バケツの水を瓶に移すのに、ティースプーンを使うようなものだと想像してください。形式上は動きますが、ひどく時間がかかります。ひしゃくやマグカップを使う方がずっと賢明です。ファイルも同じで、1文字ずつ読み書きするのは、水をスプーンで運ぶのと同じです。
例えば次のようになります:
// ファイルを1文字ずつ読む(ティースプーン方式)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// 文字を処理する(たとえば数えるだけ)
}
}
ファイルが大きいと、プログラムがとてものんびり動くと感じるはずです。
バッファリングとは何か、なぜ必要か
バッファとは、データを1文字ずつではなく、まとめて(例えば8KB以上)読み書きするための専用メモリ領域(通常は配列)です。平たく言えば、中間の「容器」のようなメモリで、データを小出しではなく塊で扱います。
仕組みはこうです。読み取りでは、プログラムが一度ディスクにアクセスしてバッファにブロック単位でデータを読み込み、その後はメモリから1文字ずつ取り出して渡します。ブロックが尽きたら次を読み込みます。書き込みでも同様で、まずデータをバッファに溜め、バッファがいっぱいになったとき、ストリームを閉じたとき、または flush() を呼んだときに、まとめてディスクへ書き出します。
なぜ速いのでしょうか。ディスク自体は遅く、とくに細かく頻繁にアクセスすると効率が悪いからです。アクセス回数を減らし、一度に大きめの塊でやり取りすれば、体感は大きく改善します。つまり、バッファリングはディスクアクセスの回数を節約し、プログラムを高速化します。
2. BufferedReader と BufferedWriter: 構文と例
Javaではテキストファイルの読み書きをバッファリングするために、次の2つのクラスを使います:
- BufferedReader — テキストファイルの読み取り用。
- BufferedWriter — テキストファイルの書き込み用。
これらは通常の Reader/Writer(例えば FileReader/FileWriter)の上位に配置され、バッファリングを追加します。
BufferedReader で行単位に読む
最も一般的なのは、ファイルを行ごとに読むケースです。メソッド readLine() は、改行文字("\n" または "\r\n")までの文字列を返します。
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 行を表示
}
} catch (IOException e) {
System.out.println("ファイルの読み取り中にエラーが発生しました: " + e.getMessage());
}
}
}
ここで何が起きているか
まず FileReader を作成し、これを BufferedReader で包みます。バッファ付きリーダーは、データを1文字ずつではなく大きな塊で取り込み、メモリに貯めたものを readLine() で行単位に渡してくれます。結果として、単純に while ループを書くだけで、ファイルがどれほど大きくても効率よく高速に読み取れます。
BufferedWriter で書き込む
文字列を効率よくファイルに書き込むこともできます:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, world!");
writer.newLine(); // 改行(OS依存)
writer.write("これは2行目。");
} catch (IOException e) {
System.out.println("ファイルの書き込み中にエラーが発生しました: " + e.getMessage());
}
}
}
ここで何が起きているか
最初に FileWriter を作り、それを BufferedWriter で包みます。write() や newLine() を呼んでも、データはすぐにディスクへは行きません。いったんバッファ(中間のメモリ)に溜まり、バッファが埋まったとき、ストリームを閉じたとき、または明示的に flush() を呼んだときに、まとめてファイルへ書き出されます。これにより書き込みが大幅に高速化され、ディスクアクセス回数も減ります。
ひとつのアプリにまとめるとどうなる?
たとえば、入力をファイルに保存しつつ画面にも表示する簡単な「日記」アプリを書いてみます。
import java.io.*;
import java.util.Scanner;
public class DiaryApp {
public static void main(String[] args) {
String fileName = "diary.txt";
Scanner scanner = new Scanner(System.in);
// 新しいエントリを書き込む
System.out.print("日記に新しいエントリを入力してください: ");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("保存しました!");
} catch (IOException e) {
System.out.println("書き込みエラー: " + e.getMessage());
}
// 全エントリを読み出す
System.out.println("\nあなたの日記:");
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("読み取りエラー: " + e.getMessage());
}
}
}
ファイル名は "diary.txt"。ユーザーに新しいエントリの入力を促したら、FileWriter を append モード(true を渡す)で使い、既存の内容を消さずに末尾へ追記します。BufferedWriter で包むことで、まずメモリにためてからまとめて書き出すため、高速かつ効率的に保存できます。
続いて、同じファイルを BufferedReader で開いて読み込みます。中では大きな塊で取り込み、外には1行ずつ渡してくれるので、ループで表示するだけで最初から最後までの「日記」を確認できます。
3. BufferedReader と BufferedWriter の利点
大幅な高速化
バッファ付きのストリームで読み書きすると、ディスクへのアクセス回数が桁違いに少なくなります。これは特に大きなファイルで顕著です。
便利なメソッド
- BufferedReader.readLine() — 行ごとに読み取れるため、テキスト処理(ログ、CSV、設定ファイルなど)に便利です。
- BufferedWriter.newLine() — OSに合わせた改行コードを適切に追加します。
使いやすさ
- 他のストリームと簡単に組み合わせられます(例えば、さまざまな文字エンコーディングで読むために InputStreamReader を BufferedReader の内側に入れる)。
- try-with-resources を使えばリソースは自動的にクローズされます。
柔軟性
- デフォルトのバッファサイズ(通常は8KB)が合わない場合、明示的にサイズを指定できます:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // 16KB のバッファ
4. BufferedReader と BufferedWriter を使うべきとき
次のときに使う:
- テキストファイル(ログ、CSV、大きなテキストデータ)を扱う場合。
- ファイルを行単位で読み書きしたい場合。
- 大きなファイルで速度が重要な場合の高速化。
- Reader/Writer をサポートするネットワークや他のソースからのストリームを処理する場合。
次のときは使わない:
- バイナリファイル(画像、アーカイブ、動画など)を扱う場合 — これは InputStream/OutputStream の出番です。
- ファイルが非常に小さく、一度に全体を読み書きするだけの場合 — バッファリングの効果は最小ですが、害もほぼありません。
5. 実用的な注意点
さまざまな文字エンコーディングとの併用
特定のエンコーディング(例: "UTF-8"、"Windows-1251")で読み書きしたい場合は、InputStreamReader/OutputStreamWriter をバッファ付きストリームと組み合わせて使います:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
明示的なフラッシュ
データを確実にディスクへ反映させたい(例えばログやレシート)場合は、writer.flush() を呼びます。通常はストリームのクローズ時に自動でフラッシュされます。
バッファサイズ
デフォルトのバッファは約8KBです。処理内容に応じて効果が見込めるなら、サイズを変更しても構いません(巨大なファイルの処理など)。
比較: FileReader/FileWriter と BufferedReader/BufferedWriter
| クラス | 大きなファイルでの速度 | 行単位の読み取りのしやすさ | 行単位の書き込みのしやすさ | エンコーディングの柔軟性 |
|---|---|---|---|---|
| FileReader/FileWriter | 遅い | いいえ(1文字ずつのみ) | いいえ(1文字ずつのみ) | デフォルトのみ |
| BufferedReader/Writer | 速い | はい (readLine()) | はい (newLine()) | はい(InputStreamReader/OutputStreamWriter 経由) |
6. BufferedReader と BufferedWriter でよくあるミス
ミス1: ストリームを閉じ忘れる。 try-with-resources を使わなかったり close() を呼び忘れたりすると、ファイルがロックされたままになったり、データがディスクに書き出されないことがあります。常に try-with-resources を使いましょう。
ミス2: テキストとバイナリを混同する。 バイナリファイル(".jpg"、".zip" など)を BufferedReader で開こうとすると、文字化けやエラーになります。バイナリには InputStream/OutputStream を使ってください。
ミス3: 大量データでバッファリングを使わない。 1文字ずつ読み書きすると、プログラムは遅くなります。大きなファイルでは必ずバッファリングを使いましょう。
ミス4: 必要な場面で flush() を呼ばない。 データをすぐにディスクへ反映させたい(たとえばログ用途)場合は writer.flush() を呼びます。通常はストリームのクローズ時に自動で実行されます。
ミス5: エンコーディングを考慮しない。 不適切なエンコーディングでファイルを開くと、テキストが正しく表示されません(ローカル言語が「?」や文字化けになるなど)。システムデフォルトと異なる場合は、必要なエンコーディング(例: "UTF-8")を明示しましょう。
GO TO FULL VERSION