File I/O & NIO.2

Reading Files

15 min Lesson 2 of 13

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:

import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.io.IOException; public class ReadStringDemo { public static void main(String[] args) throws IOException { Path path = Path.of("config.txt"); String content = Files.readString(path, StandardCharsets.UTF_8); System.out.println(content); } }

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):

import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.io.IOException; import java.util.List; public class ReadAllLinesDemo { public static void main(String[] args) throws IOException { Path path = Path.of("names.txt"); List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8); for (String line : lines) { System.out.println(line); } // or with the Stream API you already know: lines.stream() .filter(l -> !l.isBlank()) .map(String::trim) .forEach(System.out::println); } }
Key idea — what the list actually contains: 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 BufferedReader instead.

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.

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 BufferedReaderDemo { public static void main(String[] args) throws IOException { Path path = Path.of("access.log"); // Files.newBufferedReader is the NIO.2-friendly factory try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; long errorCount = 0; while ((line = reader.readLine()) != null) { if (line.contains("ERROR")) { errorCount++; System.out.println(line); } } System.out.println("Total errors: " + errorCount); } // the try-with-resources guarantees the reader is closed here } }

A few things to notice:

  • Files.newBufferedReader(path, charset) is the preferred factory over new BufferedReader(new FileReader(path)) — the latter uses the platform default encoding and requires more boilerplate.
  • readLine() returns null at end-of-file, not an exception. The while (... != 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:

import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; public class FilesLinesDemo { public static void main(String[] args) throws IOException { Path path = Path.of("access.log"); // try-with-resources is REQUIRED — the stream wraps an open reader try (var lines = Files.lines(path, StandardCharsets.UTF_8)) { List<String> errors = lines .filter(l -> l.contains("ERROR")) .collect(Collectors.toList()); System.out.println("Errors found: " + errors.size()); } } }
Do not forget try-with-resources for Files.lines. Unlike 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 textFiles.readString
  • Small file, need each line as a list elementFiles.readAllLines
  • Large file, process line by line imperativelyBufferedReader + readLine()
  • Large file, process with Stream pipelineFiles.lines inside try-with-resources
Rule of thumb on file size: if the file could plausibly exceed a few hundred megabytes in production, treat it as "large" and avoid the convenience methods. A background job that processes web-server logs, a data-import pipeline, or a build tool compiling many source files are all large-file scenarios where 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.