Java I/O
Java I/O (Input/Output) is how your program talks to the outside world — reading from files, writing data, accepting keyboard input, or sending bytes over a network. The java.io package gives you a rich toolkit of classes that model these operations as streams: a flowing sequence of data that you read from or write to one piece at a time.
Understanding I/O is essential for almost every real application, from reading a config file to persisting user data. Java’s stream model is composable — you wrap simple streams in smarter ones to add buffering, encoding awareness, or type safety.
The Stream Model
A stream is a one-directional channel of data. Java splits them into two fundamental axes:
| Axis | Types | Base classes |
|---|---|---|
| Direction | Input / Output | InputStream / OutputStream, Reader / Writer |
| Data unit | Bytes / Characters | InputStream / Reader |
All stream classes inherit from one of four abstract roots:
InputStream— read raw bytesOutputStream— write raw bytesReader— read decoded characters (Unicode-aware)Writer— write encoded characters
Tip: Always prefer character streams (
Reader/Writer) when working with text. They handle character encoding (UTF-8, etc.) correctly so you never corrupt international text.
Byte Streams vs Character Streams
Byte streams work at the raw 8-bit level — perfect for images, audio, binary protocols, and anything that isn’t plain text. Character streams sit on top of byte streams and apply a Charset so that each read() gives you a decoded char, not a raw byte.
import java.io.*;
public class ByteVsChar {
public static void main(String[] args) throws IOException {
// Byte stream: reads raw bytes
try (FileInputStream fis = new FileInputStream("data.bin")) {
int b;
while ((b = fis.read()) != -1) {
System.out.print(b + " ");
}
}
// Character stream: reads decoded characters
try (FileReader fr = new FileReader("hello.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
}
}
}
The Decorator Pattern in Action
Java I/O is a textbook example of the Decorator design pattern. You start with a basic stream and wrap it in higher-level streams to add capabilities:
import java.io.*;
public class WrappedStreams {
public static void main(String[] args) throws IOException {
// Stack: FileOutputStream → BufferedOutputStream → DataOutputStream
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("record.dat")))) {
dos.writeUTF("Alice");
dos.writeInt(30);
dos.writeDouble(72000.50);
}
System.out.println("Record written.");
}
}
Output:
Record written.
Each wrapper adds one responsibility: FileOutputStream owns the file handle, BufferedOutputStream reduces system calls by batching writes, and DataOutputStream adds type-aware writeInt/writeDouble methods.
Always Close Your Streams
Open streams hold OS file descriptors — a limited resource. Use try-with-resources (introduced in Java 7) and Java closes the stream for you, even if an exception is thrown.
// Good: try-with-resources — stream closed automatically
try (BufferedReader br = new BufferedReader(new FileReader("notes.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} // br.close() called here, always
Warning: Forgetting to close streams causes resource leaks. In long-running servers this eventually causes
Too many open fileserrors. Always use try-with-resources.
Under the Hood
How Buffering Works
Every call to FileInputStream.read() triggers a native system call — transferring control from user space to the kernel, copying a byte from the kernel’s page cache, then returning. System calls are expensive (microseconds each). BufferedInputStream allocates an internal byte array (default 8 KB) and fills it with a single system call, then satisfies subsequent read() calls from that in-memory buffer. This can reduce I/O time by an order of magnitude for sequential reads.
Character Encoding Under the Hood
InputStreamReader holds a reference to a sun.nio.cs.StreamDecoder which batches raw bytes from the underlying InputStream and runs them through a java.nio.charset.CharsetDecoder. The decoder maintains state (partial multi-byte sequences) between calls, so multi-byte UTF-8 characters split across buffer boundaries are still decoded correctly.
Streams and the JVM
Stream objects are regular heap objects. Closing a stream calls close() which delegates to the underlying OS file descriptor via a native method. The garbage collector does not reliably close streams — finalize() was deprecated in Java 9 and removed in Java 18 — so programmatic closing is mandatory.
A Complete Read-Then-Write Example
import java.io.*;
public class CopyFile {
public static void main(String[] args) throws IOException {
String src = "input.txt";
String dest = "output.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(src));
BufferedWriter writer = new BufferedWriter(new FileWriter(dest))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
System.out.println("File copied successfully.");
}
}
Output:
File copied successfully.
Note:
writer.newLine()writes the platform-appropriate line separator (\r\non Windows,\non Unix) rather than hardcoding"\n".
Standard Streams
Java wires three streams up automatically when the JVM starts:
| Field | Type | Default |
|---|---|---|
System.in | InputStream | Keyboard |
System.out | PrintStream | Console (stdout) |
System.err | PrintStream | Console (stderr) |
You can reassign them with System.setIn(), System.setOut(), and System.setErr() — useful for redirecting output in tests.
In This Section
- Byte vs Character Streams — understand the fundamental split between raw-byte and Unicode-aware text streams, and when to use each.
- FileInputStream — read raw bytes from a file on disk, one byte or a chunk at a time.
- FileOutputStream — write raw bytes to a file, with options to append or overwrite.
- BufferedInputStream — wrap any
InputStreamto add an 8 KB read buffer and dramatically cut system-call overhead. - BufferedOutputStream — batch writes to any
OutputStreamin memory before flushing to disk or a socket. - DataInputStream — read Java primitives (
int,double,boolean, etc.) from a byte stream in a portable binary format. - DataOutputStream — write Java primitives to a byte stream so
DataInputStreamcan read them back exactly. - ByteArray Streams — use an in-memory byte array as a stream source or sink — great for testing and intermediate processing.
- SequenceInputStream — concatenate multiple
InputStreams so they appear as a single continuous stream. - Object Streams — serialize and deserialize entire Java objects to and from a byte stream using
ObjectInputStream/ObjectOutputStream. - FileReader — read a text file character by character with automatic default-charset decoding.
- FileWriter — write character data to a text file, with optional append mode.
- BufferedReader — add line-buffering to any
Reader, enabling the convenientreadLine()method. - BufferedWriter — buffer character output and provide a portable
newLine()helper. - InputStreamReader / OutputStreamWriter — the bridge between byte streams and character streams; lets you specify the charset explicitly.
- PrintStream — the class behind
System.out; offersprint,println, andprintffor formatted text output to a byte stream. - PrintWriter — like
PrintStreambut built on character streams, with auto-flush support — ideal for writing formatted text to files or network connections. - Scanner — parse tokens (words, numbers, lines) from any
InputStream,Reader, orStringusing a simple, readable API. - Console — read passwords securely (no echo) and interact with the terminal via
System.console(). - Other I/O Classes — a tour of
LineNumberReader,StreamTokenizer,PushbackInputStream, and other specialist I/O utilities.
Related Topics
- File Handling — create, read, update, and delete files using the
Fileclass and modern NIO.2FilesAPI. - Serialization — deep-dive into object serialization,
Serializable,serialVersionUID, and security considerations. - NIO.2: Path & Files — the modern replacement for
File, with atomic operations, directory walking, and file watching. - Byte vs Character Streams — the essential first read before diving into any specific stream class.
- Exception Handling — I/O throws checked
IOExceptioneverywhere; learn how to handle it cleanly. - Multithreading — sharing streams across threads requires synchronization; learn why and how.