Java Tips | I/O・ネットワーク:行単位読み込み

Java Java
スポンサーリンク

なぜ「行単位読み込み」ユーティリティが業務で効いてくるのか

業務システムで扱うファイルは、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);
    }
}
Java

LineHandler 側では、

StreamingLineReader.readLines(path, StandardCharsets.UTF_8, line -> {
    String[] cols = CsvLineParser.parseSimpleCsvLine(line);
    if (cols.length < 3) {
        throw new InvalidLineException("列数が足りません: " + line);
    }
    // 正常処理
});
Java

呼び出し側では、IOExceptionInvalidLineException を分けて扱うことで、 「ファイル自体の問題」と「データ内容の問題」を区別してログに残したり、ユーザーに伝えたりできます。

ここで深掘りしたいのは、「例外を握りつぶさない」ことです。 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 設計に一歩近づきます。

タイトルとURLをコピーしました