To read a text file in Java, wrap a FileReader in a BufferedReader inside a try-with-resources block and read it line by line; to write one, use a BufferedWriter the same way — or, on Java 11+, use the one-line Files.readString(path) and Files.write(path, bytes) helpers from java.nio.file for small files. The single decision that matters is file size: streaming APIs (BufferedReader, Files.lines) keep memory flat and handle files of any size, while whole-file APIs (Files.readAllLines, Files.readString) are more concise but load everything into the heap and will throw OutOfMemoryError on a file that's too big. Always pass StandardCharsets.UTF_8 explicitly so the result is identical on Windows, Linux, and macOS.
That's the summary an AI Overview gives you. What it can't show you is which of the six-plus reading and writing methods to reach for in a given situation, or how the classes actually wrap each other. Below is a decision table that maps each method to its correct use case, an animated diagram of how bytes flow from disk through the stream stack into your program, and a copy-paste example for every path — the concrete detail that turns "it depends" into a decision you can make in ten seconds.
Which File I/O Method Should You Use?
Java gives you several ways to read and write a file, and they are not interchangeable. Pick by file size and data type first, convenience second:
| Method | Best for | Memory profile | Java version | Use when |
|---|---|---|---|---|
BufferedReader / BufferedWriter | Line-by-line text | Constant (streams) | 1.1+ | Default for text files of any size |
Files.lines(path) | Line-by-line text, functional style | Constant (lazy stream) | 8+ | You want to filter/map lines with the Stream API |
Files.readString / Files.writeString | Whole small text file | Loads entire file | 11+ | Config, template, or file well under heap size |
Files.readAllLines / Files.write | Whole small file as List<String> | Loads entire file | 7+ | You need all lines in a list at once |
Scanner | Token parsing (nextInt, nextDouble) | Small buffer | 5+ | Parsing mixed types, not just raw lines |
FileInputStream / FileOutputStream | Binary data (images, audio, serialized objects) | Constant (streams) | 1.0+ | The file is not human-readable text |
PrintWriter | Formatted / printf-style text output | Buffered | 1.1+ | Writing reports or logs with formatting |
The trap most guides skip: Files.readAllLines() and Files.readString() are the friendliest to type and the fastest way to crash a service. They pull the whole file into memory, so a 3 GB log on a container with a 512 MB heap is an instant OutOfMemoryError. If you can't guarantee the file is small, stream it.
How Bytes Flow Through Java's Stream Stack
Java's I/O classes are built with the decorator pattern — each class wraps the one below it and adds a capability. Understanding this stack is why you write new BufferedReader(new FileReader(...)) instead of one magic class:
Read the constructor call inside-out and it mirrors the diagram: FileReader sits closest to the disk and turns bytes into characters; BufferedReader wraps it to batch those reads and hand you whole lines via readLine(). Skip the buffer and every read() becomes a separate system call — which is exactly why buffered streams are the performance default.
Understanding Java I/O Streams
Java provides a powerful Input/Output (I/O) system that allows applications to read and write data efficiently. The Java I/O API is primarily based on streams, which facilitate sequential data processing. These streams can be categorized into two main types:
Stream Types
Byte Streams – Used for handling raw binary data, such as images and audio files
- Key classes:
InputStream,OutputStream,FileInputStream,FileOutputStream
Character Streams – Designed for reading and writing text-based data using Unicode encoding
- Key classes:
Reader,Writer,FileReader,FileWriter
💡 Performance Tip: Use buffered streams like BufferedReader and BufferedWriter to reduce I/O operations and improve performance when working with large files.
Reading Files in Java
Java provides multiple approaches for reading files efficiently, depending on your specific requirements. Here are the most common and effective methods:
Using BufferedReader (Recommended)
The BufferedReader class is the most efficient way to read text files, especially for larger files:
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
Using Scanner for Token-based Reading
The Scanner class offers flexibility when reading files, allowing token-based parsing and primitive type extraction:
try (Scanner scanner = new Scanner(new File("example.txt"))) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Modern Approach: Files Class (Java 7+)
With Java 7 and later, the Files class provides a convenient and modern way to read file contents. On Java 11+, Files.readString(Paths.get("example.txt")) reads a whole small file into a single String in one line:
try {
List<String> lines = Files.readAllLines(Paths.get("example.txt"), StandardCharsets.UTF_8);
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
⚠️ Memory Warning: The Files.readAllLines() method loads the entire file into memory. Use BufferedReader for large files to avoid OutOfMemoryError.
Writing Files in Java
Java provides multiple ways to write files, allowing developers to choose the best method based on performance and flexibility requirements.
Using BufferedWriter (Recommended)
The BufferedWriter class improves efficiency by reducing the number of I/O operations. To append instead of overwrite, pass true as the second argument to FileWriter:
try (BufferedWriter bw = new BufferedWriter(new FileWriter("example.txt"))) {
bw.write("Hello, World!");
bw.newLine();
bw.write("This is a test file.");
} catch (IOException e) {
e.printStackTrace();
}
// Append mode: new FileWriter("example.txt", true)
Using PrintWriter for Formatted Output
The PrintWriter class provides a convenient way to write formatted text to a file:
try (PrintWriter pw = new PrintWriter("example.txt")) {
pw.println("Hello, World!");
pw.printf("Processed %d records at %s%n", count, timestamp);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Modern Approach: Files.write (Java 7+)
Java 7 introduced the Files class, offering a modern and concise way to write files. Add StandardOpenOption.APPEND to add to an existing file rather than truncating it:
List<String> lines = Arrays.asList("Hello, World!", "This is a test file.");
try {
Files.write(Paths.get("example.txt"), lines, StandardCharsets.UTF_8);
// Append: Files.write(path, lines, UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
e.printStackTrace();
}
Handling Different File Formats
Java supports reading and writing various file formats, including binary files and structured data formats. Here are common approaches for handling different file types.
Binary Files
Binary files store non-text data, such as images, audio, and serialized objects. Use FileInputStream and FileOutputStream for handling binary file operations. Note that available() returns only an estimate of readable bytes, so for a whole file prefer Files.readAllBytes(path) (small files) or a fixed-size buffer loop:
// Reliable whole-file read (small files):
byte[] data = Files.readAllBytes(Paths.get("image.png"));
// Streaming copy for any size:
try (InputStream in = new FileInputStream("image.png");
OutputStream out = new FileOutputStream("copy.png")) {
byte[] buffer = new byte[8192];
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
e.printStackTrace();
}
JSON Files
Java does not have built-in JSON support, but libraries like Jackson and Gson simplify JSON processing. The library handles serialization; you still use ordinary file I/O to move bytes to disk:
// Writing JSON with Gson
Gson gson = new Gson();
try (Writer writer = new FileWriter("data.json")) {
MyDataObject data = new MyDataObject();
gson.toJson(data, writer);
} catch (IOException e) {
e.printStackTrace();
}
// Reading JSON with Gson
try (Reader reader = new FileReader("data.json")) {
MyDataObject data = gson.fromJson(reader, MyDataObject.class);
// Process data
} catch (IOException e) {
e.printStackTrace();
}
Best Practices for Java File Handling
Following best practices ensures efficiency, security, and maintainability when handling files in Java applications.
1. Use Try-With-Resources for Automatic Resource Management
Java provides the try-with-resources statement to automatically close file resources, preventing the resource leaks that cause "too many open files" errors and locked files on Windows:
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
2. Handle Exceptions Properly
File operations often fail due to missing files, permissions, or I/O errors. The java.nio.file API gives you granular exception types — catch the specific ones before the generic IOException:
try {
List<String> lines = Files.readAllLines(Paths.get("example.txt"));
} catch (NoSuchFileException e) {
System.err.println("File not found!");
} catch (AccessDeniedException e) {
System.err.println("Permission denied!");
} catch (IOException e) {
System.err.println("I/O error occurred: " + e.getMessage());
}
3. Specify Character Encoding
Always specify a character encoding (UTF-8) to avoid platform-specific encoding issues. Java 18 (JEP 400) made UTF-8 the default charset, but on JDK 17 and earlier FileReader/FileWriter used the platform default — so a file written on Windows could arrive garbled on Linux. Passing the charset explicitly makes the behavior identical on every JDK:
Files.write(Paths.get("example.txt"), lines, StandardCharsets.UTF_8);
📝 Important: Avoid reading large files into memory using Files.readAllLines(). For very large files, use BufferedReader to prevent OutOfMemoryError.
Conclusion
File handling is an essential component of Java programming that enables applications to read, write, and process data efficiently. Java provides multiple approaches for file operations, from traditional I/O streams to modern utilities like the Files class.
By following best practices—such as using try-with-resources, handling exceptions properly, optimizing for large files, and specifying character encoding—developers can ensure their file operations are efficient, reliable, and scalable.
Understanding these techniques allows Java developers to build robust applications that manage data seamlessly, whether dealing with configuration files, logs, structured data, or large datasets. Choose the right approach based on your specific requirements for file size, performance, and functionality.
For enterprise applications requiring robust security awareness training and compliance monitoring, proper file handling practices are crucial for maintaining data integrity and security standards.