テキストファイルの末尾からｎ行を読み込むには


テキストファイルの末尾から指定した行数のテキストを効率的に取り出したいです。  
Linux の tail コマンドのようなものです。

[code]

    File file = new File("c:¥¥tmp¥¥test.txt");
    BufferedReader br = new BufferedReader(new FileReader(file));
    
    String str;
    while((str = br.readLine()) != null){
      ・・・
    }
    
    br.close();
    
[/code]

このようなファイルの先頭から順番に１行ずつ取得する方法を応用して最後のｎ行を取り出すことは出来るのですが、この方法だとファイルサイズが大きい時に処理時間が長くなってしまいます。  
ファイルの末尾から１行ずつ取り出す方法はあるでしょうか？

対象のテキストファイルは以下の想定です。

  * 文字コードは UTF-8
  * 改行はCRLF、LFが混在
  * １行の文字数に上限なし




[Apache Commons-IO](https://commons.apache.org/proper/commons-io/) ライブラリの
[`org.apache.commons.io.input`](https://commons.apache.org/proper/commons-
io/javadocs/api-release/index.html?org/apache/commons/io/input/package-
summary.html) パッケージに参考になりそうなものがありました。

  * [`ReversedLinesFileReader`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/ReversedLinesFileReader.html): テキストファイルを末尾から読むクラス
  * [`Tailer`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/Tailer.html) : tail のようにファイル監視機能付の末尾出力の仕組み

仕様は、[`FileReader`](https://docs.oracle.com/javase/8/docs/api/java/io/FileReader.html)
に似ているので、使い方が直観的です。

ReadFromTail.java:

[code]

    import org.apache.commons.io.input.ReversedLinesFileReader;
    import java.io.*;
    
    public class ReadFromTail {
      public static void main(String[] args) throws IOException {
        try (ReversedLinesFileReader r = new ReversedLinesFileReader( new File(args[0]))) {
            System.out.println(r.readLine());
        }
      }
    }
    
[/code]

[code]

    $ javac -cp commons-io-2.4.jar ReadFromTail.java
    $ java -cp commons-io-2.4.jar ReadFromTail c:¥tmp¥test.txt
    I'm the Last Line!!!
    
[/code]

`ReversedLinesFileReader` のソースを見ると、デフォルト 4kb
でファイル全体をブロックに分割されているとしてから、末尾ブロックから読みだしていました。 読みだし位置の指定は、
[`RandomAccessFile.seek()`](http://docs.oracle.com/javase/8/docs/api/java/io/RandomAccessFile.html#seek-
long-) です。他に、UTF-8 や Shift-JIS エンコーディングに対応する部分が興味を引く感じ。

ReversedLinesFileReader.java (抜粋):

[code]

    ...
    
    randomAccessFile = new RandomAccessFile(file, "r");
    totalByteLength = randomAccessFile.length();
    int lastBlockLength = (int) (totalByteLength % blockSize);
    if (lastBlockLength > 0) {
        totalBlockCount = totalByteLength / blockSize + 1;
    } else {
        totalBlockCount = totalByteLength / blockSize;
        if (totalByteLength > 0) {
            lastBlockLength = blockSize;
        }
    }
    currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
    
    ...
    
    int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
    this.data = new byte[dataLength];
    final long off = (no - 1) * blockSize;
    
    // read data
    if (no > 0 /* file not empty */) {
        randomAccessFile.seek(off);
        final int countRead = randomAccessFile.read(data, 0, length);
    
    ...
    
[/code]




[Apache Commons-IO](https://commons.apache.org/proper/commons-io/) ライブラリの
[`org.apache.commons.io.input`](https://commons.apache.org/proper/commons-
io/javadocs/api-release/index.html?org/apache/commons/io/input/package-
summary.html) パッケージに参考になりそうなものがありました。

  * [`ReversedLinesFileReader`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/ReversedLinesFileReader.html): テキストファイルを末尾から読むクラス
  * [`Tailer`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/Tailer.html) : tail のようにファイル監視機能付の末尾出力の仕組み

仕様は、[`FileReader`](https://docs.oracle.com/javase/8/docs/api/java/io/FileReader.html)
に似ているので、使い方が直観的です。

ReadFromTail.java:

[code]

    import org.apache.commons.io.input.ReversedLinesFileReader;
    import java.io.*;
    
    public class ReadFromTail {
      public static void main(String[] args) throws IOException {
        try (ReversedLinesFileReader r = new ReversedLinesFileReader( new File(args[0]))) {
            System.out.println(r.readLine());
        }
      }
    }
    
[/code]

[code]

    $ javac -cp commons-io-2.4.jar ReadFromTail.java
    $ java -cp commons-io-2.4.jar ReadFromTail c:¥tmp¥test.txt
    I'm the Last Line!!!
    
[/code]

`ReversedLinesFileReader` のソースを見ると、デフォルト 4kb
でファイル全体をブロックに分割されているとしてから、末尾ブロックから読みだしていました。 読みだし位置の指定は、
[`RandomAccessFile.seek()`](http://docs.oracle.com/javase/8/docs/api/java/io/RandomAccessFile.html#seek-
long-) です。他に、UTF-8 や Shift-JIS エンコーディングに対応する部分が興味を引く感じ。

ReversedLinesFileReader.java (抜粋):

[code]

    ...
    
    randomAccessFile = new RandomAccessFile(file, "r");
    totalByteLength = randomAccessFile.length();
    int lastBlockLength = (int) (totalByteLength % blockSize);
    if (lastBlockLength > 0) {
        totalBlockCount = totalByteLength / blockSize + 1;
    } else {
        totalBlockCount = totalByteLength / blockSize;
        if (totalByteLength > 0) {
            lastBlockLength = blockSize;
        }
    }
    currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
    
    ...
    
    int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
    this.data = new byte[dataLength];
    final long off = (no - 1) * blockSize;
    
    // read data
    if (no > 0 /* file not empty */) {
        randomAccessFile.seek(off);
        final int countRead = randomAccessFile.read(data, 0, length);
    
    ...
    
[/code]

