Skip to content
Java io 6 min read

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:

AxisTypesBase classes
DirectionInput / OutputInputStream / OutputStream, Reader / Writer
Data unitBytes / CharactersInputStream / Reader

All stream classes inherit from one of four abstract roots:

  • InputStream — read raw bytes
  • OutputStream — write raw bytes
  • Reader — 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 files errors. 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\n on Windows, \n on Unix) rather than hardcoding "\n".

Standard Streams

Java wires three streams up automatically when the JVM starts:

FieldTypeDefault
System.inInputStreamKeyboard
System.outPrintStreamConsole (stdout)
System.errPrintStreamConsole (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 InputStream to add an 8 KB read buffer and dramatically cut system-call overhead.
  • BufferedOutputStream — batch writes to any OutputStream in 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 DataInputStream can 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 convenient readLine() 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; offers print, println, and printf for formatted text output to a byte stream.
  • PrintWriter — like PrintStream but 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, or String using 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.
  • File Handling — create, read, update, and delete files using the File class and modern NIO.2 Files API.
  • 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 IOException everywhere; learn how to handle it cleanly.
  • Multithreading — sharing streams across threads requires synchronization; learn why and how.
Last updated June 13, 2026
Was this helpful?