なぜ「行単位読み込み」ユーティリティが業務で効いてくるのか
業務システムで扱うファイルは、CSV、ログ、設定ファイルなど「1行=1レコード」「1行=1メッセージ」という形がとても多いです。 このとき、ファイル全体を一気に読み込むよりも、「行単位で順番に処理する」方が自然で、安全で、メモリにも優しいことが多いです。
例えば、100万行のCSVをインポートする処理を考えてみてください。 全部を List<String> に読み込んでから処理するより、1行ずつ読みながらバリデーションしてDBに登録していく方が、 メモリ使用量も安定し、途中でエラーが起きたときの扱いも決めやすくなります。
この「行単位読み込み」を毎回生で書くのではなく、ユーティリティとしてまとめておくと、 コードの見通しが良くなり、例外処理や文字コードの扱いも統一できます。
Java の基本的な「行単位読み込み」の形を押さえる
Files.readAllLines で「小さめのファイル」を読む
まずは、ファイルサイズがそれほど大きくない場合の基本形です。 java.nio.file.Files.readAllLines を使うと、指定した文字コードで全行を List<String> に読み込めます。
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class LineReaderUtils {
public static List<String> readAllLines(Path path, Charset charset) throws IOException {
return Files.readAllLines(path, charset);
}
}
Java使い方の例です。
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
public class ReadAllLinesExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("data/users.csv");
List<String> lines = LineReaderUtils.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
System.out.println("LINE: " + line);
}
}
}
Javaここで重要なのは、文字コード(Charset)を必ず指定していることです。 CSV や設定ファイルは、UTF-8 で統一するのがほぼ定番です。 readAllLines(path) のように文字コードを省略すると、環境依存になり、文字化けや本番と検証環境の差の原因になります。
もう一つのポイントは、「ファイル全体をメモリに載せる」という性質です。 数千〜数万行程度なら問題ありませんが、何十万〜何百万行になると、メモリ使用量が気になってきます。 そのときに登場するのが、次の「ストリームで行単位読み込みをする」形です。
大きなファイルを「行単位でストリーム読み込み」する
BufferedReader を使った基本ユーティリティ
巨大なCSVやログファイルを扱うときは、「1行ずつ読みながら処理する」スタイルが安全です。 BufferedReader を使うと、行単位で読み進めることができます。
これをユーティリティとしてまとめると、次のような形になります。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
public class StreamingLineReader {
public interface LineHandler {
void handle(String line) throws Exception;
}
public static void readLines(Path path, Charset charset, LineHandler handler) throws Exception {
try (BufferedReader reader = Files.newBufferedReader(path, charset)) {
String line;
while ((line = reader.readLine()) != null) {
handler.handle(line);
}
} catch (IOException e) {
throw e;
}
}
}
Java使い方の例です。
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class StreamingLineReaderExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("data/large.csv");
StreamingLineReader.readLines(path, StandardCharsets.UTF_8, line -> {
// ここで1行ずつ処理する
System.out.println("LINE: " + line);
});
}
}
Javaここで深掘りしたいポイントがいくつかあります。
一つ目は、「try-with-resources で必ずストリームを閉じている」ことです。 try (BufferedReader reader = ...) { ... } と書くことで、処理が終わったときに自動的に close() が呼ばれます。 ファイルを開いたまま閉じ忘れると、ファイルハンドルが枯渇して、長期運用で障害になることがあります。 ユーティリティ側でこのパターンを徹底しておくと、呼び出し側の負担を減らせます。
二つ目は、「LineHandler インターフェースで“1行ごとの処理”を呼び出し側に渡している」ことです。 これにより、ユーティリティは「読むこと」に集中し、 業務ロジックは「行をどう解釈するか」に集中できます。 責務の分離ができているので、テストや再利用がしやすくなります。
行単位読み込みと CSV・ログ処理の関係
「行をどう解釈するか」を別ユーティリティに分ける
行単位読み込みユーティリティは、「生の文字列としての行」を渡してくれます。 CSV なら「カンマ区切りをパースする」、ログなら「タイムスタンプとメッセージに分解する」といった処理は、別の層で行うのがきれいです。
例えば、CSV の1行をオブジェクトに変換する小さなユーティリティを用意します。
public class CsvLineParser {
public static String[] parseSimpleCsvLine(String line) {
// 本格的なCSVパーサではなく、単純なカンマ区切りの例
return line.split(",");
}
}
Javaこれを、先ほどの StreamingLineReader と組み合わせます。
StreamingLineReader.readLines(path, StandardCharsets.UTF_8, line -> {
String[] cols = CsvLineParser.parseSimpleCsvLine(line);
// cols[0], cols[1], ... を使って業務オブジェクトを作る
});
Javaこうして、「行単位読み込み」と「行の解釈」を分けておくと、 CSV の仕様が変わったときも、読み込み部分はそのまま、パース部分だけ差し替える、といった柔軟な対応ができます。
例外処理をどう設計するか
IOException と業務エラーの切り分け
行単位読み込みでは、主に二種類のエラーが起きます。
一つは、ファイルそのものに関するエラーです。 ファイルが存在しない、権限がない、途中で読み込みに失敗した、などで IOException が投げられます。
もう一つは、「行の内容がおかしい」という業務的なエラーです。 CSV の列数が足りない、数値に変換できない、必須項目が空、などです。
ユーティリティの設計としては、次のような方針が考えられます。
ファイル読み込みに関するエラーは IOException としてそのまま投げる。 行の内容に関するエラーは、LineHandler 内で検出し、独自の例外(例えば InvalidLineException)を投げる。
例えば、こんなイメージです。
public class InvalidLineException extends Exception {
public InvalidLineException(String message) {
super(message);
}
}
JavaLineHandler 側では、
StreamingLineReader.readLines(path, StandardCharsets.UTF_8, line -> {
String[] cols = CsvLineParser.parseSimpleCsvLine(line);
if (cols.length < 3) {
throw new InvalidLineException("列数が足りません: " + line);
}
// 正常処理
});
Java呼び出し側では、IOException と InvalidLineException を分けて扱うことで、 「ファイル自体の問題」と「データ内容の問題」を区別してログに残したり、ユーザーに伝えたりできます。
ここで深掘りしたいのは、「例外を握りつぶさない」ことです。 catch (Exception e) {} のように何もせずに無視すると、 どこで何が起きたのか分からなくなり、障害調査が難しくなります。 最低限、ログに出す・呼び出し元に伝える、といった方針を決めておきましょう。
パスの扱いとセキュリティの視点
「どこから何を読んでいるか」をユーティリティで制御する
行単位読み込みは、そのまま「外部入力」を意味します。 インポートファイルやログなど、内容によっては機密情報や個人情報が含まれます。
だからこそ、「どこから何を読んでいるか」をユーティリティ側である程度制御しておくと安全です。
例えば、アプリケーションが読み込んでよいディレクトリを決めておき、 その配下のファイルだけを対象にするような設計です。
import java.nio.file.Path;
public class AppInputPaths {
private final Path baseDir;
public AppInputPaths(Path baseDir) {
this.baseDir = baseDir;
}
public Path imports(String name) {
return baseDir.resolve("imports").resolve(name);
}
public Path config(String name) {
return baseDir.resolve("config").resolve(name);
}
}
Java業務コード側では、「必ず AppInputPaths 経由でパスを取る」というルールにしておくと、 ../../ で意図しない場所を読まれるような攻撃を防ぎやすくなります。
まとめ:行単位読み込みユーティリティで身につけてほしい感覚
行単位読み込みユーティリティは、「ファイルを1行ずつ安全に読み進める」ための小さな道具です。 しかし、その裏には次のような大事な考え方が隠れています。
小さめのファイルは readAllLines、大きなファイルは BufferedReader でストリーム読み込みする。 文字コード(Charset)を必ず指定し、UTF-8 などに統一して環境依存を避ける。 try-with-resources でストリームを必ず閉じ、リソースリークを防ぐ。 「行を読む」責務と「行を解釈する」責務を分けて、CSV やログのパースを別ユーティリティに切り出す。 ファイルエラーとデータエラーを例外で区別し、ログやユーザーへの伝え方を設計する。
もしあなたのプロジェクトで、 毎回バラバラに BufferedReader を手書きしている場所が散らばっているなら、 それを一度「行単位読み込みユーティリティ」にまとめられないか眺めてみてください。
それだけで、コードの意図がはっきりし、 大きなファイルにも耐えられる、セキュアで運用しやすい I/O 設計に一歩近づきます。
