Why this matters
- Reading a file one byte at a time instead of through a buffer can be hundreds of times slower for no visible reason in the code.
- Choosing a byte stream for text, or relying on the platform's default character set, produces corrupted output that only appears on another machine.
- Forgetting to close a resource leaks file handles, and the modern syntax that prevents it is one line.
Four families
Every classic I/O class in java.io sits in one of four boxes, defined by direction and unit.
| Aspect | Byte streams | Character streams |
|---|---|---|
| Unit | 8-bit bytes | 16-bit characters |
| Base classes | InputStream, OutputStream | Reader, Writer |
| Use for | Images, audio, zip, any binary | Text of any kind |
| Character set aware | No | Yes — decodes and encodes |
| Typical pair | FileInputStream / FileOutputStream | FileReader / FileWriter |
Unit
Byte streams8-bit bytesCharacter streams16-bit charactersBase classes
Byte streamsInputStream, OutputStreamCharacter streamsReader, WriterUse for
Byte streamsImages, audio, zip, any binaryCharacter streamsText of any kindCharacter set aware
Byte streamsNoCharacter streamsYes — decodes and encodesTypical pair
Byte streamsFileInputStream / FileOutputStreamCharacter streamsFileReader / FileWriter
Using a byte stream for text works for plain ASCII and silently breaks on anything else.
Buffering is not optional
An unbuffered read asks the operating system for data on every single call. Buffering reads a block once and serves your calls from memory.
// Slow: one system call per byte
try (FileInputStream in = new FileInputStream("data.bin")) {
int byteRead;
while ((byteRead = in.read()) != -1) {
process(byteRead);
}
}
// Fast: reads in blocks, serves from memory
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("data.bin"))) {
int byteRead;
while ((byteRead = in.read()) != -1) {
process(byteRead);
}
}
The loop is identical. The only change is wrapping the stream, and that wrapping is the difference between a program that finishes and one that appears to hang.
Always specify the character set
// Depends on the machine's default — the same code gives different results elsewhere
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) { }
// Explicit and reproducible
try (BufferedReader reader = Files.newBufferedReader(
Path.of("notes.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
Before Java 18 the default character set came from the operating system, so a file written on a developer's laptop could be unreadable on a Linux server. Java 18 made UTF-8 the default everywhere, which fixed the common case — but naming the charset explicitly is still the right habit, because it documents intent and works on every version.
try-with-resources
Any object implementing AutoCloseable can be declared in the parentheses of a try, and the
JVM closes it when the block exits — on success, on exception, on return, always.
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
BufferedWriter writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.strip());
writer.newLine();
}
}
Details worth knowing
- Multiple resources are separated by semicolons and closed in reverse order of declaration, so a wrapper closes before what it wraps.
- If the body throws and
close()also throws, the body's exception wins and the close failure is attached as a suppressed exception, reachable viagetSuppressed(). - Closing the outermost wrapper closes the whole chain. You never need to close the inner stream yourself.
Prefer java.nio.file for files
The Files and Path classes are clearer than File for almost every task, and they report
real errors instead of returning false.
Path path = Path.of("config", "app.properties");
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String whole = Files.readString(path, StandardCharsets.UTF_8);
Files.writeString(path, "key=value\n", StandardCharsets.UTF_8);
Files.createDirectories(path.getParent());
boolean exists = Files.exists(path);
// Streaming a large file without loading it into memory
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
stream.filter(line -> !line.isBlank()).forEach(System.out::println);
}
Console input
// Simple and convenient; parses as it reads
Scanner scanner = new Scanner(System.in);
System.out.print("Name: ");
String name = scanner.nextLine();
System.out.print("Age: ");
int age = scanner.nextInt();
// Faster for bulk input; you parse yourself
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
int value = Integer.parseInt(reader.readLine().strip());
Scanner is the right default for small interactive programs. For competitive programming or
bulk piped input, BufferedReader is substantially faster because Scanner does regular
expression work on every token.
Common misreadings
- "Buffering is a micro-optimisation." It changes the number of system calls by orders of magnitude. It is the difference between usable and unusable.
- "
FileReaderhandles any encoding." Before Java 18 it used the platform default; it has never let you choose. UseFiles.newBufferedReaderwith an explicit charset. - "
flush()is unnecessary." Buffered writers hold data until the buffer fills. Closing flushes, but if you never close, the tail of your output is lost. - "
File.delete()tells you what went wrong." It returnsfalsewith no reason.Files.deletethrows an exception that names the cause. - "One stream per file handle must be closed individually." Closing the outermost wrapper closes everything beneath it.
Quick recall
Everything you need if you only revisit this box.
- Two axes: bytes vs characters, input vs output.
InputStream/OutputStreamfor binary,Reader/Writerfor text. - Always wrap in a buffered variant. Unbuffered reads make one system call per byte.
- Name the character set explicitly; UTF-8 became the default only in Java 18.
- Use try-with-resources always. Resources close in reverse order, and a failing
close()is recorded as suppressed rather than hiding the real exception. - Prefer
java.nio.file.FilesandPath. UseFiles.linesfor large files and close the stream. Scannerfor small interactive input,BufferedReaderfor bulk — and never mixnextInt()withnextLine()carelessly.
Test yourself
Answer these before moving on — recall is what makes it stick.
- How do you read and write files in Java? Explain FileReader, BufferedReader, InputStream, and try-with-resources.
- How do you design caching layers and read/write splitting for Java data-intensive applications?
- What is the difference between Java IO and NIO? What are channels, buffers, and selectors in NIO?