Buffered I/O & try-with-resources
java.io vs java.nio
Java has two I/O subsystems:
java.io— stream-based, blocking I/O with a rich class hierarchyjava.nio— buffer/channel-based, supports non-blocking and memory-mapped I/O
For everyday text file I/O, java.io's reader/writer classes remain completely valid, especially in combination with try-with-resources.
Why Buffer?
FileReader and FileWriter read/write one character at a time from/to disk, which is extremely slow. Wrapping them in BufferedReader / BufferedWriter adds an in-memory buffer (default 8 KB), drastically reducing the number of system calls.
BufferedReader br = new BufferedReader(new FileReader("file.txt"));Reading Line by Line
BufferedReader.readLine() returns the next line without the terminator, or null at end-of-file. Java 8 adds lines() which returns a Stream<String>.
Writing
BufferedWriter wraps FileWriter. PrintWriter wraps BufferedWriter and adds the familiar println()/printf() methods.
Charset
FileReader and FileWriter use the platform's default encoding — a portability trap. Prefer specifying an explicit charset:
new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8)Or use Files.newBufferedReader(path, charset) / Files.newBufferedWriter(path, charset), which already buffer and accept a Charset.
Scanner for stdin
Scanner(System.in) is the simplest way to read user input. It tokenises input by whitespace by default; nextLine() reads a full line.
java.io vs java.nio Tradeoffs
| Aspect | java.io | java.nio (NIO.2) |
|---|---|---|
| API style | Stream/Reader/Writer | Path/Files/Channel |
| Non-blocking | No | Yes (via Channels) |
| Ease of use | Simple for text I/O | Simple for file ops |
| Performance | Good with buffering | Good; channels faster for large files |
| Symbolic links | Limited | Full support |
For most application code, NIO.2 (Files.readString, Files.writeString) is the best choice. Use java.io when integrating with libraries that expect streams or when building interactive console apps.
Code Examples
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 BufferedReadDemo {
public static void main(String[] args) throws IOException {
// Create a temporary file for the demo
Path path = Files.createTempFile("buffered-read", ".txt");
Files.writeString(path,
"First line\nSecond line\nThird line\nFourth line");
// Style 1: Manual readLine() loop (classic)
System.out.println("-- readLine() loop --");
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
int lineNum = 1;
while ((line = br.readLine()) != null) {
System.out.println(lineNum++ + ": " + line);
}
} // br.close() called automatically
// Style 2: Stream of lines (Java 8+)
System.out.println("-- lines() stream --");
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
br.lines()
.filter(l -> l.startsWith("S") || l.startsWith("F"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
// Files.lines() — even more concise (also returns Stream<String>)
System.out.println("-- Files.lines() --");
try (java.util.stream.Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.limit(2).forEach(System.out::println);
}
Files.deleteIfExists(path);
}
}Files.newBufferedReader() is preferred over new BufferedReader(new FileReader()) because it accepts an explicit charset. All three patterns close the reader automatically via try-with-resources.
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class BufferedWriteDemo {
public static void main(String[] args) throws IOException {
Path path = Files.createTempFile("buffered-write", ".log");
// BufferedWriter via Files.newBufferedWriter
try (BufferedWriter bw = Files.newBufferedWriter(
path, StandardCharsets.UTF_8,
StandardOpenOption.TRUNCATE_EXISTING)) {
bw.write("Line 1");
bw.newLine(); // platform-neutral line separator
bw.write("Line 2");
bw.newLine();
}
// PrintWriter wraps BufferedWriter — adds println/printf
try (PrintWriter pw = new PrintWriter(
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.APPEND))) {
pw.println("Line 3 via PrintWriter");
pw.printf("Pi is approximately %.4f%n", Math.PI);
}
// Verify
System.out.println("Written content:");
Files.lines(path, StandardCharsets.UTF_8).forEach(System.out::println);
System.out.println("Total lines: " + Files.lines(path, StandardCharsets.UTF_8).count());
Files.deleteIfExists(path);
}
}BufferedWriter.newLine() uses the platform's line separator, ensuring portability. PrintWriter.printf() provides C-style formatting. Both are closed automatically by try-with-resources, and the buffer is flushed on close.
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.InputMismatchException;
import java.util.Scanner;
public class ScannerDemo {
// In a real app you would pass System.in; here we simulate it
static Scanner simulatedInput(String text) {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
return new Scanner(new ByteArrayInputStream(bytes));
}
public static void main(String[] args) {
// --- Reading tokens ---
try (Scanner sc = simulatedInput("Alice 30 3.14\nBob 25 2.71")) {
while (sc.hasNext()) {
String name = sc.next();
int age = sc.nextInt();
double val = sc.nextDouble();
System.out.printf("Name: %-6s Age: %d Val: %.2f%n", name, age, val);
}
}
// --- Reading full lines ---
try (Scanner sc = simulatedInput("Hello World\nJava I/O")) {
while (sc.hasNextLine()) {
System.out.println("Line: " + sc.nextLine());
}
}
// --- Input validation ---
try (Scanner sc = simulatedInput("42\nabc\n17")) {
while (sc.hasNext()) {
if (sc.hasNextInt()) {
System.out.println("Int: " + sc.nextInt());
} else {
System.out.println("Skipped: " + sc.next());
}
}
}
// Real-world pattern (commented out to avoid blocking):
// try (Scanner stdin = new Scanner(System.in)) {
// System.out.print("Enter name: ");
// String name = stdin.nextLine();
// System.out.println("Hello, " + name + "!");
// }
System.out.println("Scanner demo complete");
}
}Scanner wraps any InputStream and tokenises it. hasNextInt() / hasNextLine() guard against InputMismatchException. In production code, wrap System.in in a Scanner (never close it, as that closes stdin permanently).
Quick Quiz
1. Why should you wrap FileReader with BufferedReader rather than using FileReader directly?
2. What is the risk of using `FileReader` without specifying a charset?
3. What does `BufferedWriter.newLine()` write?
4. What happens when you call `scanner.close()` on a Scanner wrapping `System.in`?
Was this lesson helpful?