Use Files.newBufferedReader(path, StandardCharsets.UTF_8) when you want a straightforward Java loop that processes one text line at a time. Use Files.lines(path, StandardCharsets.UTF_8) when a stream operation such as filtering or counting makes the code clearer. Reserve Files.readAllLines for files whose size you know is small enough to hold in memory.
All three read text lines, not arbitrary binary records or complete multiline log events. The examples below work on Java 17 and later.
Read each line with BufferedReader
This complete program opens the file named on the command line, prints each line, and closes the reader even if reading fails:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadLines {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Usage: java ReadLines <file>");
return;
}
Path path = Path.of(args[0]);
try (BufferedReader reader =
Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
Save it as ReadLines.java, then run:
javac ReadLines.java
java ReadLines app.log
readLine() returns a String while it can read a line and returns null at end-of-file. Do not use reader.ready() as an EOF test: it reports whether a read is guaranteed not to block, not whether the file has ended. The BufferedReader API defines these behaviors.
The loop prints lines for demonstration. Replace System.out.println(line) with the work your program actually needs, such as checking a prefix or counting a match. Keep the work inside the loop if you do not need every line afterward; collecting each line into another list would give up the main memory benefit.
What counts as a line?
BufferedReader.readLine() recognizes line feed (\n), carriage return (\r), and carriage return followed by line feed (\r\n). The returned string does not include those termination characters. An empty line is returned as an empty string; it is not the same as null, which signals EOF. A final line without a trailing newline can still be returned before EOF.
For example, reading this file:
INFO started
ERROR disk full
INFO stopped
produces four strings: "INFO started", "ERROR disk full", "", and "INFO stopped". If you need to preserve the exact original newline bytes or distinguish \n from \r\n, a line reader is the wrong tool. Read bytes or characters with an API that retains the separators.
A BufferedReader avoids loading the entire file into a list, but it does not impose a maximum line length. One enormous line can still require a large String. If input is untrusted or a single physical line can be huge, define an input-size rule and use a bounded parser rather than assuming “line by line” means constant memory in every case.
Use Files.lines for a stream pipeline
Files.lines lazily supplies lines to a Stream<String>. It is useful when the operation is naturally expressed as a filter, mapping step, or count. For example, to count physical lines containing the exact text ERROR, add import java.util.stream.Stream; and use:
try (Stream<String> lines =
Files.lines(path, StandardCharsets.UTF_8)) {
long count = lines.filter(line -> line.contains("ERROR")).count();
System.out.println(count);
}
This example can replace the reader block in ReadLines.main; it uses the same path variable. contains("ERROR") is case-sensitive and is only a text match, not a structured severity parser.
The stream holds an open file. Close it with try-with-resources even if a terminal operation such as count() finishes normally. The Files API says the file is closed when the stream closes. It also says that an I/O or decoding error encountered after the stream is created is wrapped in UncheckedIOException during consumption. That differs from the explicit IOException used by the BufferedReader loop.
A stream does not make an eager operation magically memory-efficient. Calling .toList() on all its lines would still retain all of them. Use Files.lines when the result can stay small, such as a count, a bounded selection, or a summary.
Use readAllLines only for a known small file
Files.readAllLines(path, StandardCharsets.UTF_8) returns a List<String> containing the entire file’s lines:
import java.util.List;
// Inside a method where path is a Path:
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
This is convenient for a small configuration fixture or test input when you genuinely need random access to several lines. It is a poor default for a log archive or any file that can grow without a clear limit. Oracle’s Files documentation explicitly says readAllLines is not intended for large files.
The method closes the file after reading, so there is no returned reader or stream to close. It still has to allocate memory for the resulting list and strings. Decide whether “small” is actually guaranteed by the input contract, not by the size of today’s sample file.
| Need | Prefer |
|---|---|
| Simple sequential processing | Files.newBufferedReader and readLine() |
| Lazy filter, mapping, or count | Files.lines inside try-with-resources |
| All lines from a size-bounded file | Files.readAllLines |
| Token or delimiter parsing as well as lines | Scanner |
When Scanner is useful
Scanner is designed for parsing tokens using delimiters and patterns. Choose it if a text file contains values you want to read as numbers or words, or if delimiter handling is more important than a plain line loop. It also offers hasNextLine() and nextLine() when you need line-based access.
For example, new Scanner(path, StandardCharsets.UTF_8) uses an explicit charset; that constructor has been available since Java 10. Put a file-backed scanner in try-with-resources so it closes its source. For simple line traversal, BufferedReader is usually clearer because it exposes the EOF and I/O behavior directly. If a Scanner read ends unexpectedly, check scanner.ioException(): Scanner can treat an underlying IOException as the end of input. Avoid presenting Scanner as a universally faster or slower choice without measuring the actual workload.
See the Scanner API for its token methods and delimiter rules. Do not mix nextInt() and nextLine() casually: the line separator left after a token read can produce an unexpected empty line in the next call.
Specify the file’s real charset
The examples use StandardCharsets.UTF_8 because the sample file is assumed to contain UTF-8 text. An explicit charset makes that assumption visible. If the source file uses another encoding, select its actual charset instead of forcing UTF-8 and treating a decoding failure as corrupt data.
Older Java guidance often says FileReader’s default charset varies by machine. That needs a version boundary. In JDK 18 and later, Java’s default charset is UTF-8 across operating systems unless changed by JVM configuration or implementation-specific behavior; in JDK 17 and earlier, it could depend on the environment. Oracle’s migration guide documents the change. Specifying the expected charset remains useful across versions and still matters when the file itself is not UTF-8.
Files.newBufferedReader(path, charset) throws an IOException if it encounters a malformed or unmappable byte sequence while reading. Do not silently ignore that exception or claim the file was completely processed. With Files.lines, the equivalent error may surface later as UncheckedIOException while the stream is consumed. Both cases deserve an explicit failure or recovery policy if the file is part of a larger job.
A line is not always one log event
A Java exception can produce a message followed by many stack-frame lines. readLine() correctly returns those as separate physical lines; it does not know they belong to one logical event. The same issue can appear with multiline application messages.
If you must reconstruct events, define a rule based on the log format—such as a timestamped first line followed by continuation lines—and bound the maximum event size and wait time. A rule that blindly joins every line until the next timestamp can misgroup data when timestamps appear inside messages or are missing. For a simple one-event-per-line format, no multiline assembly is needed.
This guide reads a file that already exists. It does not implement tail -f: the loop stops when it reaches EOF. Following later appends, detecting rotation, and handling a file modified while it is being read require separate design. The Files.lines API specifically says modifying file contents during a terminal stream operation leaves its result undefined.
If the file contains logs you need to investigate, see how to read logs. Fluxtail’s log management features can help search logs after a supported source sends them; reading a local file with this Java code does not itself forward or guarantee delivery of any event.