6
\$\begingroup\$

Print the tail of a file, if the file has less than 10 lines, then everything available is printed.

Any suggestions for improvement are welcome.

package je3.io;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * Created by IDEA on 29/01/15.
 */
public class Tail {
    public static String[] tailLines(String filename, int nLinesToRead) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile(filename, "r");
        long lengthOfFile = randomAccessFile.length();
        long counterFromEnd = 1L;
        long newlineCounterGoal = nLinesToRead;
        int newlineCounter = 0;
        long tailPosition = 0L; // start of the end ;-)

        // If you want to get the last 10 lines,
        // and the last line ends with a newline, then you need to count back 11 newlines
        // if there is no trailing newline, then you only need to count back 10
        randomAccessFile.seek(lengthOfFile - 1L);
        char currentChar = (char) randomAccessFile.readByte();
        if(currentChar == '\n') {
            newlineCounterGoal++;
        }

        while(counterFromEnd <= lengthOfFile) {
            randomAccessFile.seek(lengthOfFile - counterFromEnd);
            if(randomAccessFile.readByte() == '\n') {
                newlineCounter++;
            }
            if(newlineCounter == newlineCounterGoal) {
                tailPosition = randomAccessFile.getFilePointer();
                break;
            }
            counterFromEnd++;
        }
        randomAccessFile.seek(tailPosition);

        String line;
        String[] lines = new String[nLinesToRead];
        int nLine = 0;
        while((line = randomAccessFile.readLine()) != null) {
            lines[nLine++] = line;
        }
        return lines;
    }

    public static void main(String[] args) throws IOException {
        if(args.length != 1) {
            throw new IllegalArgumentException();
        }
        String[] lines = tailLines(args[0], 10);
        for(String line : lines) {
            if(line != null) {
                System.out.println(line);
            }
        }
    }
}
\$\endgroup\$
11
\$\begingroup\$

Obviously the tail of the file is harder to do. This is a much more challenging situation than getting the first 10 lines. You have chosen a system that requires complicated byte manipulation to get bytes in to chars, and I don't believe the system will work for non-ascii files where the character encoding of the data may have a second-byte of 0x0A causing a mis-parse of a new-line.

For simplicity sake I would still recommend you use a forward cursor, and a circular buffer. This will work for all files regardless of the size, but the performance will be related to the size. On my system though, even 20MB files were done within moments (0.3 seconds according to the bash 'time' command). So that are not too large (a couple of megabytes), and is easy:

String[] lines = new String[10];
int count = 0;
String line = null;
while ((line = bufferedReader.readLine()) != null) {
    lines[count % lines.length] = line;
    count++;
}

That will load as many as possible lines in to the lines array, limited to 10. It will have the 10-most-recent lines.

Then you can dump those with:

int start = count - 10;
if (start < 0) {
    start = 0;
}

for (int i = start; i < count; i++) {
    System.out.println(lines[i % lines.length];
}

The above code will always work, but for larger files will be slow.

For practical purposes though, I don't believe you will run in to problems, and you should be fine.

I worked a little Java 8 in to the following code as well. Consider the RingBuffer class which allows you to collect the data in to it:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;


public class Tail {

    private static final class RingBuffer {
        private final int limit;
        private final String[] data;
        private int counter = 0;

        public RingBuffer(int limit) {
            this.limit = limit;
            this.data = new String[limit];
        }

        public void collect(String line) {
            data[counter++ % limit] = line;
        }

        public List<String> contents() {
            return IntStream.range(counter < limit ? 0 : counter - limit, counter)
                    .mapToObj(index -> data[index % limit])
                    .collect(Collectors.toList());
        }

    }

    public static final List<String> tailFile(final Path source, final int limit) throws IOException {

        try (Stream<String> stream = Files.lines(source)) {
            RingBuffer buffer = new RingBuffer(limit);
            stream.forEach(line -> buffer.collect(line));

            return buffer.contents();
        }

    }

    public static void main(String[] args) throws IOException {
        tailFile(Paths.get(args[0]), 10).forEach(line -> System.out.println(line));
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.