Reading Files
Reading Files
Java gives you several ways to read a file, and picking the right one matters. The wrong choice can either load gigabytes into heap memory or leave unclosed streams scattered across your code. This lesson covers the three main approaches — Files.readString, Files.readAllLines, and BufferedReader — and explains exactly when to reach for each one.
The Two Convenience Methods: readString and readAllLines
Since Java 11, java.nio.file.Files ships two static helpers that collapse file reading to a single line. Both sit on top of NIO.2 and handle resource cleanup automatically.
Files.readString reads the entire file into one String:
The second argument — the Charset — is optional; if you omit it, Java uses the platform's default encoding. Always pass StandardCharsets.UTF_8 explicitly so your code behaves identically on Windows, Linux, and macOS.
Files.readAllLines reads every line into a List<String>, stripping the line terminators (\n, \r\n, \r):
readAllLines gives you a plain ArrayList. Line terminators are stripped, blank lines become empty strings, and the last line is included even if the file does not end with a newline. The list is fully in memory before the first line is processed.
The Trade-off: Convenience vs. Memory
Both helpers are convenient, but they share a critical characteristic: the entire file is loaded into memory before you can touch a single byte. For a 5 KB config file or a 200 KB CSV that is perfectly fine. For a 2 GB log file it is a heap explosion waiting to happen.
Files.readString— best for small structured text (config, JSON, XML, templates) that you want as one blob.Files.readAllLines— best for modest line-oriented files where you want random access to any line, or you need to pass the full list to another method.- Neither is appropriate for large files. Use
BufferedReaderinstead.
BufferedReader for Large Files
BufferedReader reads a configurable chunk (default 8 KB) from disk at a time and hands lines to you one by one. Your application's memory usage stays roughly constant regardless of file size.
A few things to notice:
Files.newBufferedReader(path, charset)is the preferred factory overnew BufferedReader(new FileReader(path))— the latter uses the platform default encoding and requires more boilerplate.readLine()returnsnullat end-of-file, not an exception. Thewhile (... != null)idiom is idiomatic Java.- The
try-with-resources block closes the reader even if an exception is thrown mid-file — essential for correctness with large files.
Streaming Lines with Files.lines
If you are comfortable with the Stream API, Files.lines (Java 8+) gives you a lazy Stream<String> that pairs naturally with filter, map, and collect:
readAllLines, the stream holds an open file handle. If you skip the try-with-resources block, the handle leaks until the GC finalizes the stream — which may never happen in a long-running application, eventually exhausting the OS file-descriptor limit.
Choosing the Right Tool
- Small file, need the whole text →
Files.readString - Small file, need each line as a list element →
Files.readAllLines - Large file, process line by line imperatively →
BufferedReader+readLine() - Large file, process with Stream pipeline →
Files.linesinside try-with-resources
BufferedReader or Files.lines is the right default.
Summary
Files.readString and Files.readAllLines offer clean one-liners for small files at the cost of loading everything into memory. BufferedReader via Files.newBufferedReader gives you constant-memory line-by-line processing for arbitrarily large files. Files.lines bridges the gap when you want lazy streaming with the full power of the Stream API. Always specify the charset explicitly and always close I/O resources with try-with-resources.