ファイルを行単位で読む

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • Files.readAllLines(path) で全行を List<String> に読み込み (Java 8+)
  • Files.lines(path)Stream<String> ─ 大きいファイルでもメモリ効率がいい
  • Files.linestry-with-resources 必須 ─ ファイルハンドルを閉じないと OS リソース漏れ
  • 文字コードは StandardCharsets.UTF_8 を明示するのが安全

ファイルを行単位で読む とは

Java NIO.2 の Files.readAllLinesFiles.lines を使い、ファイルの中身を 1 行ずつ List<String> として読み込む方法を学ぶ。

ファイルを「行のリスト」として扱う

業務アプリでも個人ツールでも、ファイルを扱うときに一番よく出てくるのが「テキストファイルを 1 行ずつ読みたい」というニーズです。CSV のヘッダ行を読みたい、ログファイルの最終行だけを覗きたい、設定ファイルの内容をまるごとメモリに展開したい、用途はさまざまですが、共通しているのは「行という単位で扱いたい」という点です。

Java には伝統的に FileReader + BufferedReader を組み合わせて 1 行ずつ readLine() する書き方がありましたが、Java 7 で導入された java.nio.file.Files クラスを使うと、たった 1 行で「ファイル全体を List<String> に変換する」ことができます。本レッスンでは、この新しい NIO.2 API の中心人物である Files.readAllLinesFiles.lines を順番に押さえていきましょう。

NIO.2 は "New I/O 2" の略で、Java 7 で大きく刷新された I/O API のことです。古い java.io.File と違って Path という別の型を使うのが特徴で、シンボリックリンクや属性、UTF-8 などのエンコーディング指定もスマートに扱えます。

diagram (will load when visible)

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.readAllLinesIOException という検査例外 (checked exception) を投げる可能性があります。なので呼び出し側のメソッドには throws Exceptionthrows IOException を付けるか、try-catch で囲む必要があります。今回の課題でも throws Exception を活用しています。

デフォルトのエンコーディングは UTF-8

Java 7 の readAllLinesCharset を必ず指定する 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_JISEUC-JP を読まされる場面はまだまだあるので、エンコーディング指定の書き方は手癖で書けるようにしておくと安心です。

Files.lines — 大きいファイルは Stream で

readAllLines はとても便利ですが、ひとつ大きな弱点があります。すべての行を一度にメモリへ展開してしまう という点です。数 KB のファイルなら問題なくても、100MB を超えるログファイルを readAllLines で読むと、ヒープがあっという間に膨れ上がって OutOfMemoryError を吐くこともあります。

そこで活躍するのが Files.lines(Path) です。こちらは Stream<String> を返し、ファイルを 1 行ずつ遅延読み込み してくれます。forEachfilter を組み合わせれば、メモリ消費を抑えたまま巨大ファイルを処理できます。

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)
ストリーム自前で IteratorFiles.lines(path)
文字列まるごとBufferedReader で連結Files.readString(path)
書き込みFileWriterFiles.writeString(path, s)
一時ファイルFile.createTempFileFiles.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.linestry-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 を明示。

やってみよう

今回の課題は次のとおりです。

  1. 引数 content を一時ファイルに書き出す (Files.createTempFile + Files.writeString)
  2. その一時ファイルを Files.readAllLines で読み返す
  3. 戻り値の List<String>size()int として return する
  4. 後始末として 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.writeStringFiles.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 つができることを確認しましょう。

  1. readAllLines の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. readAllLines とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. java.nio.file.Files.createTempFile で一時ファイルを作成し、その Path を使うこと
  2. Files.writeString(path, content) で引数の文字列を書き込み、Files.readAllLines(path) で読み返すこと
  3. 読み返した 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

ヒント

main.java
main.java
学習モード

メモ

ファイルを行単位で読む

⌘S で保存