2022-05-25 2022-05-28

【Java】ファイル読み込み方法7選と速度比較【1億行読み込んで、最も速いのは?】

メイン画像

Javaでファイル読み込み処理を実装する際に、色々方法があって何を使ったらよいか悩みますよね。私もそうでした。

この記事では、「テキストファイルを読み込むのに一番良い方法って何なの?」という疑問に答えていきます。

ファイルの読み込み方法をそれぞれ解説しつつ、一番速度が速い(性能が良い)方法をご紹介します。

速さの計測方法ですが、実際に1億レコードのファイルを作成して、読み込みました。

この記事を書いている私は、Java歴6年近くのエンジニアになります。

◎この記事のまとめ

➊実際に動かした結果「FileInputStream」を使用した読み取り方法が一番速度が速い(性能が良い)

➋「NIO.2 BufferedReader(Files.newBufferedReader)」を使用するとソースが簡単(可読性が高い)で速度も2位である

➌1000万レコードのファイルと1億レコードのファイルを読み込んだが、1位2位は変動なし。
※1000万行の場合、速度差がほぼないため、環境の負荷等で順位が前後する

➍テキストファイルの読み込み速度なので、Byteとして読み込む場合は、結果が変わる可能性あり。

まとめの根拠は記事内でお話しているので、良かったら最後まで読んでみてください。

それでは本題に入っていきます。

1. ファイル読み込み方法7選:Javaテキストファイル読み込みのベストは?

 
それでは、ファイルの読み込み方法7選を一つ一つ解説していきます。

1-1. FileInputStreamを使用する

System.currentTimeMillis()を使用して、開始と終了時の現在時刻を取得して、その差分(ms)で速度を出しています。

1番目の処理にだけ速度計測処理入れていますが、2番目以降では省略しています。

String filePath = "c:\\tmp\\test.txt";

// FileInputStreamでファイルを読み込み
try (FileInputStream fis = new FileInputStream(filePath);
    BufferedReader br =
        // InputStreamReaderでstreamを文字列として読み込む
        new BufferedReader(new InputStreamReader(fis))) {

    // 時間計測用 - 開始時間(次から省略)
    long startTime = System.currentTimeMillis();

    String str;
    // BufferedReaderのreadLineメソッドで一行ずつ取り出す
    while((str = br.readLine()) != null){
        // ファイルの中身を一行ずつ出力
        System.out.println(str);
    }

    // 時間計測用 - 終了時間(次から省略)
    long endTime = System.currentTimeMillis

    // かかった時間を出力(ms)
    System.out.println("処理時間:" + (endTime - startTime) + " ms");
} catch (Exception e) {
    // 例外処理
}

 
「FileInputStream」でファイルをバイナリファイルとして読み込み、「InputStreamReader」でテキストファイルとして読み込みます。

「new InputStreamReader(fis, "UTF-8")」という書き方をすることによって、文字コードを指定して読み込めます。

上のコードでは、何も指定していないのでデフォルトの文字コードになります。デフォルトは環境ごとに異なります。例えばwindowsの場合、デフォルトは「MS932」

テキストとして一行ずつ取り出して、文字列として出力するために、「BufferedReader」を使用しています。※1-4とは別の方法で生成

ちなみに一文字ずつ取り出したい場合は「FileReader」が使用できます。

BufferedInputStreamを使用

この方法でも速度を計測したので、後ほど共有します。FileInputStream生成後に以下一行を追加しました。

BufferedInputStream bis = new BufferedInputStream(is);

1-2. NIO.2 InputStreamを使用

String filePath = "c:\\tmp\\test.txt";
try (InputStream is = Files.newInputStream(Path.of(filePath));
        BufferedReader br = new BufferedReader(new InputStreamReader(is))) {

    String str;
    while((str = br.readLine()) != null){
        System.out.println(str);
    }

} catch (Exception e) {
    // 例外処理
}

 
NIO.2(java.nio.file パッケージ参照)の「InputStream」でファイルを読み込んで処理する方法です。

1-3. NIO.2 Files.readAllLinesを使用

String filePath = "c:\\tmp\\test.txt";
try {
    // Files.readAllLinesでファイルを読み込み、forRachで一行ずつ出力
    Files.readAllLines(Path.of(filePath)).forEach(s -> {
      System.out.println(s);
    });
} catch (Exception e) {
    // 例外処理
}

 
Files.readAllLinesメソッドでファイルを読み込む方法です。引数を増やすことで、文字コード(Charset)も指定できます。

readAllLinesは、メソッドの戻り値として、List を返却するので、それをそのままループしています。

デメリット

ファイル内容をメモリに持ってしまう
⇒メモリ2Gでは1億行のレコードを読み込めない。

1-4. NIO.2BufferedReaderを使用する

String filePath = "c:\\tmp\\test.txt";

// NIO.2BufferedReaderを使用
try (BufferedReader br = Files.newBufferedReader(Path.of(filePath))) {

    String str;
    while((str = br.readLine()) != null){
        System.out.println(str);
    }

} catch (Exception e) {
    e.printStackTrace();
}

NIO.2の「Files.newBufferedReader」 でファイルを読み込んで「BufferedReader」を生成して文字列として出力しています。

引数に文字コード(Charset)を追加することで、文字コードを指定可能。

1-5. FileReaderを使用する

String filePath = "c:\\tmp\\test.txt";
// FileReaderを使用
try (FileReader reader = new FileReader(filePath);
        BufferedReader br = new BufferedReader(reader)) {

    String str;
    while((str = br.readLine()) != null){
        System.out.println(str);
    }

} catch (Exception e) {
    // 例外処理
}

「FileReader」でファイルを読み込んで「BufferedReader」で文字列として出力する方法です。

「FileReader」でも一文字ずつ出力できますが、大容量データを読み込む場合、処理が遅くなることがあるので、メモリにためて一行ずつ出力できる「BufferedReader」を使います。

ロジックを他の処理と合わせて計測したいという理由あります。

デメリット

・文字コードを指定できない。

1-6. FileChannelを使用する

String filePath = "c:\\tmp\\test.txt";

try (FileInputStream fis = new FileInputStream(new File(filePath));
        // FileChannelを生成
        FileChannel fc = fis.getChannel()) {

    ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

    while (true) {
        // バッファのクリア
        byteBuffer.clear();

        // FileChannelから読み込み
        // 読みこんだバイト数分だけpositionが移動
        // 最後まで読み込んだ場合,戻り値が-1になる
        if (fc.read(byteBuffer) < 0) {
            break;
        }

        // このメソッドで、読み込んだ位置までpositionを移動
        // これをしないと、ファイルを読み込む位置がおかしくなる。
        byteBuffer.flip();

        // 文字列として出力する
        System.out.println(StandardCharsets.UTF_8.decode(byteBuffer));
    }

} catch (Exception e) {
    e.printStackTrace();
}

FileInputStreamの「FileChannel」を使用してファイルを読み込んで出力しています。

ByteBufferを最終的にUTF-8(ファイルの元の文字コード)に変換して出力しています。

デメリット

・ソースコードの書き方が色々とある

・大きいファイルだと一度に読み込めず、ループ処理が必要
⇒byteのサイズを指定して、サイズごとに処理=1レコードが分割されて出力される時あり
⇒嫌であれば別の処理を入れる必要性

1-7. MappedByteBufferを使用する

String filePath = "c:\\tmp\\test_1bil.txt";

CharBuffer charBuffer = null;
Path pathToRead = Paths.get(filePath);

try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(
  pathToRead, EnumSet.of(StandardOpenOption.READ))) {

    MappedByteBuffer mappedByteBuffer = fileChannel
      .map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

    if (mappedByteBuffer != null) {
        charBuffer = Charset.forName("UTF-8").decode(mappedByteBuffer);
    }
    System.out.println(charBuffer.toString());

} catch (Exception e) {
    // TODO 自動生成された catch ブロック
    e.printStackTrace();
}

上の「FileChannel」の応用的な部分です。

「FileChannel」の「MappedByteBuffer」を使用します。

デメリット

・大きいファイルを読むのには不向き

・ソースコードが煩雑

2. ファイル読み込みの速度(性能)比較

速度比較した結果を共有します。

前提として、以下の条件で計測しました。

・ファイルの行数は1000万行と1億行でそれぞれ計測

・ファイルの中身は、一行36桁。UUIDで値が被らないようにした。

・ファイルサイズは1000万:411MB、1億行:3.79GB。

・実行環境はeclipseでメモリは最大2GBに指定

読み込んだファイルの作成方法は3章に書きました。

2-1. 各読み込み方法の速度比較結果

順位は1位が一番速度が速く、それ以降順番になっています。

読み込み処理速度一覧
順位 処理 1億行の速度(ms) 1000万行の速度(ms)
1 FileInputStream 552870 47037
2 NIO.2 BufferedReader 555224 47291
3 NIO.2 InputStream 590056 47570
4 FileInputStream
※BufferedInputStream
593491 50899
5 FileReader 608938 49332
6 NIO.2 Files.readAllLines × 51971
7 FileChannel 628130 56045
8 MappedByteBuffer × 58878

※数100msの誤差の場合、環境負荷状況で順位が変化する可能性あり
※1億行の速度を基準にソート、次に1000万行
※×はメモリ不足で計測不可
3位と4位は、1000万行だと順位逆転する
 

3. 一億レコード(一行36文字)作成したJavaソース

// 環境ごとに書き換えてください。
String outputPath = "c:\\tmp\\test_1bil.txt";
// ファイルに書き込むため、FileWriterを生成
try(FileWriter filewriter = new FileWriter(new File(outputPath))) {

    int i;
    for (i = 0; i < 100000000; i++) {
        // ランダム文字列としてUUIDを使用
        String randomStr = UUID.randomUUID().toString();

        // ファイルに改行コード付きでランダム文字列を書き込む。
        filewriter.write(randomStr + "\n");

    }
} catch (Exception e) {
    e.printStackTrace();
}

ランダムなUUIDを生成して、それをファイルに出力しているだけになります。

4. まとめ

最初にも載せましたがまとめです!

➊実際に動かした結果「FileInputStream」を使用した読み取り方法が一番速度が速い(性能が良い)
※「BufferedInputStream」をかますと、少し遅くなる。

➋「NIO.2 BufferedReader」を使用するとソースもシンプル(可読性が高い)で速度も2位である(Files.newBufferedReader)

➌1000万レコードのファイルと1億レコードのファイルを読み込んだが、1位2位は変動なし。
※1000万行の場合、速度差がほぼないため、環境の負荷等で順位が前後する

➍テキストファイルの読み込み速度なので、Byteとして読み込む場合は、結果が変わる可能性あり。

結果「FileInputStream + InputStreamReader + BufferedReader」で読み込んだ1-1のソースコードが一番速かったです。

ファイルが大きくなるごとに速度差が出そうですが、1000万行(400MB)くらいのファイルであれば、2位のNIO.2 BufferedReaderの方法で実装しても良いかもしれません。ソースが短くて読みやすいので。

データをメモリに保持しちゃうようなタイプのモノは将来性とかも考慮して、使わない方がよさそうですね。ただ、もっと小さいファイルだったら検討してみても良いかも?

ということで、今回大きめのファイル読み込み時の速度比較をしてみました。実際に動かしてみると、想像とちょっと違った結果になったりしますね。

環境によって速度が変わることがあるかもしれませんが、参考になれば幸いです。最後まで読んでいただき、ありがとうございました!

書き込みの速度比較に関してはこちらに書いています。もしよかったら。
【Java】ファイル書き込み方法5選と速度比較【何が一番はやいのか】

Scroll to Top