ファイルを行単位で読む
このレッスンで分かること
Files.readAllLines(path)で全行をList<String>に読み込み (Java 8+)Files.lines(path)はStream<String>─ 大きいファイルでもメモリ効率がいいFiles.linesはtry-with-resources必須 ─ ファイルハンドルを閉じないと OS リソース漏れ- 文字コードは
StandardCharsets.UTF_8を明示するのが安全
ファイルを行単位で読む とは
Java NIO.2 の
Files.readAllLinesとFiles.linesを使い、ファイルの中身を 1 行ずつList<String>として読み込む方法を学ぶ。
ファイルを「行のリスト」として扱う
業務アプリでも個人ツールでも、ファイルを扱うときに一番よく出てくるのが「テキストファイルを 1 行ずつ読みたい」というニーズです。CSV のヘッダ行を読みたい、ログファイルの最終行だけを覗きたい、設定ファイルの内容をまるごとメモリに展開したい、用途はさまざまですが、共通しているのは「行という単位で扱いたい」という点です。
Java には伝統的に FileReader + BufferedReader を組み合わせて 1 行ずつ readLine() する書き方がありましたが、Java 7 で導入された java.nio.file.Files クラスを使うと、たった 1 行で「ファイル全体を List<String> に変換する」ことができます。本レッスンでは、この新しい NIO.2 API の中心人物である Files.readAllLines と Files.lines を順番に押さえていきましょう。
NIO.2 は "New I/O 2" の略で、Java 7 で大きく刷新された I/O API のことです。古い
java.io.Fileと違ってPathという別の型を使うのが特徴で、シンボリックリンクや属性、UTF-8などのエンコーディング指定もスマートに扱えます。
Files.readAllLines — 全行を一気に読む
一番シンプルなのが Files.readAllLines(Path) です。引数の Path で指定したファイルを開き、すべての行を List<String> にまとめて返してくれます。改行文字 (\n や \r\n) はあらかじめ取り除かれているので、要素として並ぶのは「行の中身」だけです。
Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Demo {
public static void main(String[] args) throws Exception {
Path p = Path.of("notes.txt");
List<String> lines = Files.readAllLines(p);
System.out.println(lines.size());
for (String line : lines) {
System.out.println(line);
}
}
}Path.of("notes.txt") は、Java 11 以降で Paths.get("notes.txt") の代わりに使える短い書き方です。String から Path を作るときに最近のコードではよく見かけます。
readAllLines は内部で BufferedReader を作って、最後まで readLine を繰り返し、ArrayList に詰めてくれている、というイメージです。完了した時点でファイルは閉じられているので、close() を自分で呼ぶ必要はありません。リソース管理の苦労が消えるのが NIO.2 の嬉しいところです。
Files.readAllLinesはIOExceptionという検査例外 (checked exception) を投げる可能性があります。なので呼び出し側のメソッドにはthrows Exceptionかthrows IOExceptionを付けるか、try-catchで囲む必要があります。今回の課題でもthrows Exceptionを活用しています。
デフォルトのエンコーディングは UTF-8
Java 7 の readAllLines は Charset を必ず指定する readAllLines(Path, Charset) のみで、省略できませんでした。Java 8 で引数なしの readAllLines(Path) が新たに追加され、このオーバーロードは追加当初から常に UTF-8 で読み込む設計になっています。
もし別のエンコーディングのファイルを読みたい場合は、第 2 引数に Charset を渡します。
Java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
List<String> sjis = Files.readAllLines(
Path.of("legacy.txt"),
java.nio.charset.Charset.forName("Shift_JIS")
);
List<String> utf8 = Files.readAllLines(
Path.of("modern.txt"),
StandardCharsets.UTF_8
);業務ファイルで Shift_JIS や EUC-JP を読まされる場面はまだまだあるので、エンコーディング指定の書き方は手癖で書けるようにしておくと安心です。
Files.lines — 大きいファイルは Stream で
readAllLines はとても便利ですが、ひとつ大きな弱点があります。すべての行を一度にメモリへ展開してしまう という点です。数 KB のファイルなら問題なくても、100MB を超えるログファイルを readAllLines で読むと、ヒープがあっという間に膨れ上がって OutOfMemoryError を吐くこともあります。
そこで活躍するのが Files.lines(Path) です。こちらは Stream<String> を返し、ファイルを 1 行ずつ遅延読み込み してくれます。forEach や filter を組み合わせれば、メモリ消費を抑えたまま巨大ファイルを処理できます。
Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
try (Stream<String> lines = Files.lines(Path.of("big.log"))) {
long errorCount = lines.filter(l -> l.contains("ERROR")).count();
System.out.println(errorCount);
}Files.lines は内部にファイルハンドルを抱えているので、必ず try-with-resources で囲んで明示的にクローズしてください。readAllLines と違って、自動で閉じてくれるわけではありません。
BufferedReader 直書きとの対比
伝統的な書き方と比べてみると、Files API のコンパクトさがよく分かります。
Java
// 伝統的な書き方
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
List<String> result = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader("notes.txt"))) {
String line;
while ((line = br.readLine()) != null) {
result.add(line);
}
}これと等価な NIO.2 の書き方が List<String> result = Files.readAllLines(Path.of("notes.txt")); のたった 1 行です。最近の Java では、特に理由がなければ Files 系を使うのが標準と覚えてしまって構いません。
| やりたいこと | 旧来 (java.io) | NIO.2 (java.nio.file) |
|---|---|---|
| 行のリスト | BufferedReader + while ループ | Files.readAllLines(path) |
| ストリーム | 自前で Iterator | Files.lines(path) |
| 文字列まるごと | BufferedReader で連結 | Files.readString(path) |
| 書き込み | FileWriter | Files.writeString(path, s) |
| 一時ファイル | File.createTempFile | Files.createTempFile(prefix, suffix) |
今回の課題でも、一時ファイル作成は Files.createTempFile を、書き込みは Files.writeString を、読み込みは Files.readAllLines を、それぞれ NIO.2 で揃えていきます。
行数の数え方と末尾改行の扱い
Files.readAllLines が返す List<String> の size() を取れば、それがそのまま行数になります。ここで多くの初学者がつまずくのが、末尾改行の扱い です。
"a\nb\nc"(末尾改行なし) → 行は["a", "b", "c"]でsize()は3"a\nb\n"(末尾改行あり) → 行は["a", "b"]でsize()は2""(空文字列) → 行は[]でsize()は0"hello"(改行なし 1 行) → 行は["hello"]でsize()は1
つまり「最後の \n の後ろは行として数えない」というのが Java の readAllLines の仕様です。Unix 系のテキストファイルは末尾に必ず改行を付ける慣習なので、a\nb\n のような形でも「2 行のファイル」として扱うのが普通です。直感に反するように感じても、これが標準的な挙動だと覚えておいてください。
「最終行に改行があるかないか」は、
Gitの diff 表示で\ No newline at end of fileという警告メッセージを見たことがあるかもしれません。あれはまさにこの末尾改行の有無を判定している例で、多くのツールが慣習として末尾改行を付ける運用になっています。
よくある間違い
初学者が Files.readAllLines で踏みやすい落とし穴を 3 つまとめておきます。
- 巨大ファイルに
readAllLinesを使う —readAllLinesは全行を一気にメモリへロードします。1GBのログを読もうとするとOutOfMemoryErrorで死にます。サイズ感が分からない場合は最初からFiles.lines+Streamで書く癖を付けるのが安全です - エンコーディングを指定せず化ける — Java 8 以降は引数なしで
UTF-8ですが、ファイル自体がShift_JISで保存されていれば文字化けします。読み込みが化けたら、まずファイルのエンコーディングを疑いCharset.forName("Shift_JIS")を渡してみましょう - 空ファイルで
NullPointerExceptionを期待する —Files.readAllLinesは空ファイルでもnullではなく 空のListを返します。if (lines == null)のような防衛は不要で、代わりにif (lines.isEmpty())で判定するのが正解です
もう一つの細かい落とし穴として、Path.of("...") に渡すパスは OS のパスセパレータに依存する点があります。Windows で "C:\\foo\\bar.txt" のように書くか、"C:/foo/bar.txt" でもどちらでも動きますが、ハードコードは避けて Path.of("foo", "bar.txt") のように分割して書くと移植性が上がります。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- ログ解析バッチ ─
Files.lines(path).filter(line -> line.contains("ERROR")).count()でエラー件数集計 - CSV 取り込み ─ ヘッダー行をスキップしてデータ行を変換、
Files.lines(path).skip(1).map(...) - 設定ファイル読み込み ─
application.propertiesの各行をパースして Map に格納 - マイグレーションスクリプト ─ SQL ファイルを行単位で読んで実行する、DB 移行ツールの基本
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
readAllLinesで巨大ファイルを読んでいる ─ 1 GB のログをListに乗せると OOM。Files.linesでストリーミングFiles.linesをtry-with-resourcesなしで使っている ─ ファイルハンドル漏れ。コンパイラ警告も出る- 文字コード未指定 ─ Java 8 までの古い API はプラットフォーム依存。
UTF_8を明示するよう指摘される - 例外を全部呑む ─
IOExceptionを catch して空リストを返すと、本番で原因不明のデータ欠落になる
パフォーマンス考慮事項
readAllLinesはメモリに全部乗せる ─ 数 MB ならOK、数百 MB は危険Files.linesは遅延評価 ─count()forEachの終端まで実際の読み込みが始まらないBufferedReader.lines()も同等 ─ 直接BufferedReaderを作ってlines()を呼んでも同じ- 並列読み込みの限界 ─ ファイル IO は単一スレッドが多くの場合最速。
parallelStreamは CPU バウンドの処理用
ここまでの要点
Files.readAllLines は小さいファイル、Files.lines は大きいファイル + try-with-resources。文字コードは UTF_8 を明示。
やってみよう
今回の課題は次のとおりです。
- 引数
contentを一時ファイルに書き出す (Files.createTempFile+Files.writeString) - その一時ファイルを
Files.readAllLinesで読み返す - 戻り値の
List<String>のsize()をintとしてreturnする - 後始末として
Files.deleteで一時ファイルを消す
例として、次のような入力に対する出力を確認してみましょう。
countLines("a\nb\nc")は3を返す (3 行)countLines("")は0を返す (0 行)countLines("hello")は1を返す (改行なし 1 行)countLines("a\nb\n")は2を返す (末尾改行は数えない)
慣れてきたら、Files.readAllLines の代わりに Files.lines(p).count() で書き直してみたり、エンコーディングを StandardCharsets.UTF_8 明示で渡してみてください。書き方を増やしておくと、実務で大きなファイルに出会ったときに自然と Stream 版へ切り替えられるようになります。次のレッスンでは、書き込み側の Files.writeString と Files.write を本格的に取り上げます。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は Path でファイルパスを扱う で、Path でファイルパスを扱う を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- readAllLines の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. readAllLines とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
java.nio.file.Files.createTempFileで一時ファイルを作成し、そのPathを使うことFiles.writeString(path, content)で引数の文字列を書き込み、Files.readAllLines(path)で読み返すこと- 読み返した
List<String>のsize()をintとしてreturnすること (Files.lines(p).count()を使う場合は(int)でキャストする)
入出力例
test-cases.txt
countLines("a
b
c") → 3
countLines("") → 0
countLines("hello") → 1
countLines("a
b
") → 2
countLines("
") → 1