BufferedReader
In short: A character stream that buffers read access and enables line-by-line reading (readLine()) — usually wraps a FileReader or InputStreamReader.
In more detail: Without buffering, every single character access could potentially trigger an expensive system call — BufferedReader reads larger blocks into memory at once and passes them on from there, which is noticeably faster especially for many small read operations.
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}In Depth
BufferedReader implements the decorator pattern: it “wraps” another Reader (usually FileReader) and adds buffering without changing its code. The default buffer size is 8192 characters, but can be adjusted in the constructor:
try (BufferedReader br = new BufferedReader(new FileReader("log.txt"), 16384)) {
String line;
int lineCount = 0;
while ((line = br.readLine()) != null) {
lineCount++;
if (line.contains("ERROR")) {
System.out.println("Line " + lineCount + ": " + line);
}
}
} catch (IOException e) {
System.err.println("Error while reading: " + e.getMessage());
}readLine() returns null as soon as the end of the file is reached — that’s the usual exit condition for the read loop (while ((line = br.readLine()) != null)), not an exception. For very large files, where you don’t want to hold all lines in memory at once, this line-by-line reading is significantly more memory-efficient than e.g. Files.readAllLines(), which loads the entire file into a List<String> at once.
Why buffering makes such a difference
Without BufferedReader, a FileReader would potentially trigger a system call to the operating system on every single read() call, to fetch a single character from disk — for a file with thousands of characters, that would be thousands of expensive system calls. BufferedReader instead reads an entire block (default: 8192 characters) into an internal memory buffer at once, and serves subsequent read accesses directly from this buffer until it’s empty and a new block is loaded. For large files, the speed difference can be a factor of 10–100.
Character stream vs. byte stream
BufferedReader belongs to the family of character streams (Reader/Writer), which are designed for text encoding (e.g. UTF-8, automatically converts bytes into characters). For raw binary data (images, ZIP files), there are byte streams instead (InputStream/OutputStream), which don’t apply any character encoding — see I/O Streams for the overview.
Alternative: Streams API and NIO
Since Java 8, Files.lines(Path.of("file.txt")) offers a functional alternative that returns a Stream<String> line by line and combines well with filter()/map():
try (var lines = Files.lines(Path.of("log.txt"))) {
long errorCount = lines.filter(l -> l.contains("ERROR")).count();
System.out.println(errorCount + " error lines found");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}Internally, Files.lines() also uses buffering — but BufferedReader remains the classic, universally understood foundation on which many other I/O patterns in Java are built.
See also: I/O Streams, BufferedWriter, try-with-resources