Javaにおいて、外部ファイルからデータを読み込む処理は、アプリケーション開発において避けては通れない基本的な機能の一つです。

設定ファイルの読み込み、ログの解析、CSVデータの処理など、その用途は多岐にわたります。

しかし、Javaには歴史的な経緯から非常に多くのファイル操作クラスが存在しており、「どのクラスをどの場面で使うのが最適なのか」という判断は、初心者から中級者にとって非常に難しい課題となっています。

古くからある java.io パッケージのクラスから、Java 7で登場した NIO.2 (New I/O) 、さらには Java 11 以降で追加された便利なメソッドまで、Javaのファイル読み込みは進化を続けています。

本記事では、現在の Java 開発において標準的に利用されるファイル読み込みの手法を網羅的に解説し、パフォーマンスやメモリ効率を考慮した最適な使い分けについて詳しく説明します。

Javaにおけるファイル読み込みの全体像

Javaでファイルを読み込むためのAPIは、大きく分けて 「ストリーム (Stream)」「リーダー (Reader)」 の2つの系統に分類されます。

これらを正しく理解することが、最適な実装への第一歩です。

バイトストリームと文字ストリームの違い

Javaでは、データの扱い方によって以下の2種類のストリームを使い分けます。

バイトストリーム (InputStream)

画像、音声、実行ファイルなどの バイナリデータ を 8ビット(1バイト)単位で読み込むための仕組みです。

文字ストリーム (Reader)

テキストファイルを読み込むための仕組みです。

Java内部の文字コード(Unicode)とファイルの文字コードを適切に変換しながら読み込みます。

テキストファイルを読み込む場合でも、内部的にはバイトストリームとして読み込まれたデータを、文字エンコーディングに基づいて変換して処理しています。

かつてはこれらの低レベルなクラスを手動で組み合わせる必要がありましたが、現在はより抽象度の高い便利なクラスが提供されています。

NIO.2 (java.nio.file) の重要性

Java 7 以降、java.nio.file パッケージを中心とした NIO.2 という新しいAPIが登場しました。

従来の java.io.File クラスに比べて、エラーハンドリングが詳細であること、ファイルパスの操作が柔軟であること、そしてパフォーマンスが高いこと が特徴です。

現代の Java 開発では、特別な理由がない限り NIO.2 をベースとした実装が推奨されます。

現代的な手法:Filesクラスによる一括読み込み

比較的小さなサイズのファイルであれば、java.nio.file.Files クラスを使用するのが最もシンプルで効率的です。

Java 11 以降では、さらに簡潔なメソッドが追加されています。

Files.readString による全内容の取得 (Java 11+)

ファイルの内容を一つの文字列として一括で取得したい場合に最も便利なのが Files.readString です。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ReadStringExample {
    public static void main(String[] args) {
        // 読み込むファイルのパスを指定
        Path path = Paths.get("example.txt");

        try {
            // ファイルの全内容を文字列として読み込む (UTF-8を指定)
            String content = Files.readString(path, StandardCharsets.UTF_8);
            
            System.out.println("--- ファイル内容表示 ---");
            System.out.println(content);
        } catch (IOException e) {
            // ファイルが存在しない、読み取り権限がないなどのエラー処理
            System.err.println("ファイルの読み込みに失敗しました: " + e.getMessage());
        }
    }
}
実行結果
--- ファイル内容表示 ---
こんにちは、Javaのファイル読み込みの世界へ!
これはテスト用のテキストファイルです。

このメソッドは内部で適切にリソースの解放を行ってくれるため、「try-with-resources」構文を意識せずに一行で記述できる点が魅力です。

ただし、数GB(ギガバイト)を超えるような巨大なファイルを読み込むと OutOfMemoryError が発生する可能性があるため、注意が必要です。

Files.readAllLines による行ごとの取得

ファイルを1行ずつのリストとして取得したい場合は、Files.readAllLines が適しています。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;

public class ReadLinesExample {
    public static void main(String[] args) {
        Path path = Paths.get("data.csv");

        try {
            // 全ての行をList<String>として取得
            List<String> lines = Files.readAllLines(path);
            
            for (int i = 0; i < lines.size(); i++) {
                System.out.println((i + 1) + "行目: " + lines.get(i));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

この手法は、CSVファイルなどの「行」に意味があるデータを処理する際に非常に直感的です。

大規模ファイルに対応する:Stream APIの活用

数万行、数百万行に及ぶ巨大なファイルを扱う場合、全てのデータを一度にメモリ上に展開してはいけません。

そこで活用されるのが、Java 8 で導入された Files.lines メソッドです。

Files.lines による遅延読み込み

Files.lines は、ファイルを一行ずつ読み込みながら Stream<String> を返します。

これにより、メモリ消費を抑えつつ、関数型プログラミングのスタイルでデータ加工が可能になります。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;

public class LargeFileReadExample {
    public static void main(String[] args) {
        Path path = Paths.get("large_log_file.log");

        // Streamはリソース解放が必要なため try-with-resources を使用する
        try (Stream<String> lines = Files.lines(path)) {
            // "ERROR" を含む行だけを抽出して最初の10件を表示
            lines.filter(line -> line.contains("ERROR"))
                 .limit(10)
                 .forEach(System.out::println);
                 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

この方法の最大のメリットは、「ファイル全体をメモリに載せない」ことです。

一行読み込んでは処理し、次の行へ進むという「イテレーティブ」な動作をするため、メモリ効率が極めて高いのが特徴です。

伝統的かつ詳細な制御:BufferedReader

NIO.2 が普及する前からの標準的な手法であり、現在でも多くの現場で見かけるのが BufferedReader を使用する方法です。

BufferedReader の基本的な使い方

BufferedReader は、内部にバッファ(一時的な蓄え)を持つことで、ディスクへのアクセス回数を減らし、読み込みを高速化します。

Java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class BufferedReaderExample {
    public static void main(String[] args) {
        String fileName = "config.properties";

        // try-with-resources を使用して確実に close する
        try (BufferedReader br = new BufferedReader(new FileReader(fileName, StandardCharsets.UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) {
                // 1行ずつ処理を行う
                processLine(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void processLine(String line) {
        System.out.println("Processing: " + line);
    }
}

なぜ BufferedReader が選ばれるのか

現代では Files.linesFiles.newBufferedReader を使うことが増えていますが、BufferedReader には以下の利点があります。

きめ細かな制御

read() メソッドを使用して一文字ずつ読み込む、特定の文字数分だけ読み飛ばすといった低レベルな操作が可能です。

古いJava環境との互換性

Java 7 未満の環境(非常に稀ですが)でも動作します。

バイナリファイルの読み込み

画像や動画、シリアライズされたオブジェクトなどのバイナリデータを扱う場合は、Reader系統ではなく InputStream系統を使用します。

Files.readAllBytes

小さなバイナリファイルを一括で読み込む場合に最適です。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

public class BinaryReadExample {
    public static void main(String[] args) {
        Path path = Paths.get("image.png");

        try {
            // 全バイトデータを配列として取得
            byte[] fileBytes = Files.readAllBytes(path);
            System.out.println("ファイルサイズ: " + fileBytes.length + " bytes");
            
            // 例: 先頭の数バイトを確認してファイル形式をチェックするなどの処理
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

BufferedInputStream による逐次読み込み

大きなバイナリファイル(数GBの動画ファイルなど)を読み込む場合は、バッファ付きの入力ストリームを使用します。

Java
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedBinaryReadExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("video.mp4"))) {
            byte[] buffer = new byte[8192]; // 8KBのバッファ
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                // 読み込んだデータ(bytesRead分)を処理
                // 例: 別のファイルに書き出す、ネットワーク経由で送信するなど
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

特定の形式を解析する:Scanner クラス

ファイルから特定のデータ型(数値や単語)を抽出したい場合は、java.util.Scanner が便利です。

Java
import java.util.Scanner;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

public class ScannerExample {
    public static void main(String[] args) {
        Path path = Paths.get("data.txt"); // 内容: "ID: 101 Name: Java Score: 95.5"

        try (Scanner scanner = new Scanner(path)) {
            while (scanner.hasNext()) {
                if (scanner.hasNextInt()) {
                    System.out.println("数値を発見: " + scanner.nextInt());
                } else if (scanner.hasNextDouble()) {
                    System.out.println("浮動小数を発見: " + scanner.nextDouble());
                } else {
                    System.out.println("文字列を発見: " + scanner.next());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
注意点

Scanner は非常に高機能ですが、正規表現を使用してトークンを分割するため、他の手法に比べて処理速度が遅い傾向があります。

大規模なデータの読み込みには向きません。

パフォーマンスと手法の比較表

各手法の特性をまとめると以下の通りです。

手法メモリ消費記述の簡潔さ向いている用途導入バージョン
Files.readString高(一括読込)★★★★★小さなテキストファイルJava 11
Files.readAllLines高(一括読込)★★★★☆行単位の小さなファイルJava 7
Files.lines★★★★☆巨大なログ・テキストJava 8
BufferedReader★★★☆☆詳細な制御が必要な場合Java 1.1
Files.readAllBytes★★★★☆小さなバイナリデータJava 7
Scanner★★★★☆数値や特定文字のパースJava 1.5

ファイル読み込み時の重要なベストプラクティス

Javaでファイルを安全かつ効率的に読み込むためには、コードの書き方以外にも注意すべき点がいくつかあります。

1. try-with-resources を必ず使用する

ファイルなどの外部リソースは、OSによってオープンできる数に限りがあります。

読み込みが終わった後に close() を忘れると、「ファイルディスクリプタ漏れ(リソースリーク)」が発生し、アプリケーションが新しいファイルを開けなくなる致命的なバグの原因となります。

Java 7 以降は、以下の try-with-resources 構文を使用することで、ブロックを抜ける際に自動的に close() が呼ばれることが保証されます。

Java
// () 内で宣言されたリソースは自動で閉じられる
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    // 処理
} catch (IOException e) {
    // 例外処理
}

2. 文字エンコーディングを明示する

「自分の環境では動くのに、サーバー(Linux)にデプロイすると文字化けする」というトラブルは、ファイル読み込みにおいて最も多い問題の一つです。

これは、デフォルトの文字コードがOS環境に依存するために起こります。

Java 18 以降ではデフォルトが UTF-8 に統一されましたが、それ以前のバージョンや、意図的に別のエンコーディング(Shift_JISなど)で保存されたファイルを扱う場合は、必ず StandardCharsets を指定しましょう。

3. 例外処理(IOException)の設計

ファイル読み込みは、「ファイルが存在しない」「読み取り権限がない」「ディスクが故障している」といった、プログラム側では制御できない外部要因に左右されます。

単に e.printStackTrace() を書くのではなく、「ファイルがない場合にデフォルト値を返すのか」「呼び出し元にエラーを伝播させて処理を中断するのか」といったエラーハンドリングの設計を適切に行う必要があります。

現場で役立つテクニック

CSVファイルの読み込み

業務システムで頻出するCSVファイルの読み込みですが、単純なカンマ区切りであれば String.split(",") で十分な場合もあります。

しかし、「値の中にカンマが含まれる」「改行が含まれる」といった複雑なCSVを自前でパースするのは非常に困難です。

そのような場合は、無理に標準ライブラリだけで解決しようとせず、Apache Commons CSVOpenCSV などの信頼できるライブラリの導入を検討してください。

実行環境のパスに注意する

Paths.get("data.txt") のように相対パスで記述した場合、その起点は「プログラムを実行したときのカレントディレクトリ」になります。

IDE(EclipseやIntelliJ)から実行する場合と、jarファイルとしてコマンドラインから実行する場合で、参照先が変わってしまうことがあります。

設定ファイルなどは、クラスパス(src/main/resourcesなど)から読み込む getResourceAsStream を利用する手法も一般的です。

Java
// クラスパス上のファイルを読み込む例
try (InputStream is = getClass().getClassLoader().getResourceAsStream("config.yml")) {
    if (is == null) {
        throw new IOException("ファイルが見つかりません");
    }
    // InputStreamとしての処理
}

まとめ

Javaにおけるファイル読み込みは、その目的やデータの規模に応じて最適な道具を選ぶことが重要です。

  • 「手軽に短く書きたい」なら、Files.readString (Java 11+)。
  • 「巨大なファイルを効率よく処理したい」なら、Files.lines (Stream API)。
  • 「バイナリデータを扱いたい」なら、Files.readAllBytes
  • 「細かな制御やパースが必要」なら、BufferedReaderScanner

これらの使い分けをマスターすることで、堅牢でパフォーマンスの高いJavaアプリケーションを構築できるようになります。

特に try-with-resources によるリソース管理と、文字エンコーディングの明示は、プロのエンジニアとして必ず習慣化しておきましょう。

近年のJavaは、より簡潔に、より安全に I/O 処理が書けるよう進化しています。

新しいAPIを積極的に活用し、モダンなコーディングスタイルを取り入れてみてください。