Skip to content
Java io 6 min read

InputStreamReader / OutputStreamWriter

Raw byte streams like FileInputStream and FileOutputStream know nothing about character encoding — they just move bytes. InputStreamReader and OutputStreamWriter are the bridge classes that sit between byte streams and character streams, converting bytes to characters (and back) using a specified charset such as UTF-8. Any time you read text from a network socket, an HTTP response body, or a file opened as a raw InputStream, you’ll use one of these two classes.

Why the Bridge Exists

Java’s I/O library is split into two families: byte streams (InputStream/OutputStream) and character streams (Reader/Writer). Byte streams are universal — every data source ultimately produces bytes. Character streams add charset-aware encoding and decoding on top.

The gap between the two families is filled by:

ClassDirectionWraps
InputStreamReaderbytes → chars (read)any InputStream
OutputStreamWriterchars → bytes (write)any OutputStream

Both live in java.io and have been available since Java 1.1. Java 11 added convenient factory methods on InputStream and OutputStream directly (see below).

InputStreamReader

InputStreamReader extends Reader. It reads bytes from an underlying InputStream and decodes them into Java char values using a named charset.

Creating an InputStreamReader

import java.io.*;
import java.nio.charset.StandardCharsets;

// Wrap System.in (a byte stream) to read keyboard text as UTF-8 characters
InputStreamReader isr = new InputStreamReader(System.in, StandardCharsets.UTF_8);

You can also pass the charset name as a String, but using StandardCharsets constants avoids a checked UnsupportedEncodingException.

Warning: If you omit the charset argument, Java uses the platform default charset — which varies between operating systems and JVM settings. Always specify the charset explicitly to avoid subtle bugs when your code runs on different machines.

Reading Characters

import java.io.*;
import java.nio.charset.StandardCharsets;

public class ReadStdIn {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(System.in, StandardCharsets.UTF_8);

        System.out.println("Type something and press Enter:");
        StringBuilder sb = new StringBuilder();
        int ch;
        while ((ch = isr.read()) != -1 && (char) ch != '\n') {
            sb.append((char) ch);
        }
        System.out.println("You typed: " + sb);
        isr.close();
    }
}

Output:

Type something and press Enter:
Hello, Java!
You typed: Hello, Java!

Reading character by character is rarely the best approach. In practice you almost always wrap InputStreamReader in a BufferedReader for efficient line-by-line reading.

Reading a File with InputStreamReader

import java.io.*;
import java.nio.charset.StandardCharsets;

public class ReadFileISR {
    public static void main(String[] args) throws IOException {
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(
                        new FileInputStream("notes.txt"),
                        StandardCharsets.UTF_8))) {

            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

This three-layer chain — FileInputStreamInputStreamReaderBufferedReader — is the classic Java idiom for reading text files with explicit charset control.

Tip: For simple file reading with a known charset, Files.newBufferedReader(path, charset) from NIO.2 is more concise. InputStreamReader shines when you already have an InputStream (for example, from a network connection or Process.getInputStream()).

Checking the Encoding

InputStreamReader isr = new InputStreamReader(System.in, StandardCharsets.UTF_8);
System.out.println(isr.getEncoding()); // prints "UTF8"

getEncoding() returns the historical IANA name stored internally by Java (e.g., "UTF8" rather than "UTF-8"). It’s useful for debugging but not for passing back to constructors — use StandardCharsets constants for that.

OutputStreamWriter

OutputStreamWriter extends Writer. It encodes Java char values into bytes using a specified charset and writes them to an underlying OutputStream.

Creating an OutputStreamWriter

import java.io.*;
import java.nio.charset.StandardCharsets;

// Write a file in UTF-8
OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("output.txt"),
        StandardCharsets.UTF_8);

Writing Characters

import java.io.*;
import java.nio.charset.StandardCharsets;

public class WriteFileOSW {
    public static void main(String[] args) throws IOException {
        try (OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream("greeting.txt"),
                StandardCharsets.UTF_8)) {

            osw.write("Hello, World!\n");
            osw.write("Unicode test: 中文\n"); // Chinese characters
        }
        System.out.println("File written.");
    }
}

Output:

File written.

The file greeting.txt will contain properly encoded UTF-8 bytes, including the Chinese characters.

Wrapping with BufferedWriter

Just like InputStreamReader, OutputStreamWriter benefits enormously from buffering:

import java.io.*;
import java.nio.charset.StandardCharsets;

public class BufferedWrite {
    public static void main(String[] args) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(
                        new FileOutputStream("log.txt", true), // append mode
                        StandardCharsets.UTF_8))) {

            bw.write("Application started.");
            bw.newLine(); // writes OS-appropriate line separator
            bw.write("All systems go.");
            bw.newLine();
        }
    }
}

Tip: BufferedWriter.newLine() writes \r\n on Windows and \n on Unix/macOS, which is usually what you want for log files. If you need a consistent line ending regardless of OS, write "\n" explicitly.

Java 11 Convenience

Java 11 added InputStream.transferTo(OutputStream) and also InputStreamReader via InputStream directly, but the most relevant addition is that Reader and Writer gained nullReader() / nullWriter() factory methods. More practically, for many use cases NIO.2 helpers are cleaner:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

// Java 11+: read all lines from a file with explicit charset (NIO.2 path)
Path path = Path.of("notes.txt");
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    br.lines().forEach(System.out::println);
}

// Java 11+: write text with explicit charset (NIO.2 path)
try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
    bw.write("Written via NIO.2");
}

Files.newBufferedReader and Files.newBufferedWriter internally create an InputStreamReader / OutputStreamWriter, so you get the same charset handling with less boilerplate.

Common Charset Values

StandardCharsets ConstantCanonical NameUse Case
StandardCharsets.UTF_8UTF-8Universal default — use this unless you have a reason not to
StandardCharsets.ISO_8859_1ISO-8859-1Legacy Latin-1 files, some HTTP responses
StandardCharsets.US_ASCIIUS-ASCIIConfiguration files, pure ASCII protocols
StandardCharsets.UTF_16UTF-16Windows BOM-prefixed text, some binary protocols

Note: UTF-8 is the right choice for almost every new project. It encodes all Unicode code points, is backward-compatible with ASCII, and is the default charset for Java source files since Java 18.

Under the Hood

InputStreamReader holds a reference to a sun.nio.cs.StreamDecoder internally. StreamDecoder maintains a small byte buffer (typically 8 KB) and uses a java.nio.charset.CharsetDecoder to convert byte sequences into characters. The CharsetDecoder handles multi-byte sequences correctly — for example, a single UTF-8 character may span 1–4 bytes, and the decoder correctly reassembles them even when bytes arrive in separate read() calls.

OutputStreamWriter works symmetrically through a sun.nio.cs.StreamEncoder and a CharsetEncoder.

Because each call to read() or write() on an unbuffered InputStreamReader or OutputStreamWriter may invoke the underlying InputStream.read() or OutputStream.write(), which are system calls, wrapping with BufferedReader/BufferedWriter is essential for any I/O beyond trivial amounts. See the byte vs character streams page for a broader discussion of this layered design.

The default buffer size for BufferedReader and BufferedWriter is 8,192 characters. You can tune it via the constructor if you are reading very large lines or working in a memory-constrained environment.

Quick Comparison: ISR vs FileReader

FileReader is a convenience subclass of InputStreamReader that opens a file path directly, but — importantly — it used the platform default charset before Java 11. Since Java 11, FileReader constructors that accept a Charset argument were added, making it more flexible. Even so, InputStreamReader remains the go-to choice when you already have an InputStream from a non-file source (network, process, ZIP entry, etc.).

FeatureFileReaderInputStreamReader
Opens a file by pathYesNo (wraps existing stream)
Accepts any InputStreamNoYes
Charset constructor (Java 11+)YesYes (all versions)
Use whenReading a local fileWrapping any byte stream
  • Byte vs Character Streams — understand why the bridge classes exist in the first place
  • BufferedReader — wrap InputStreamReader for efficient line-by-line reading
  • BufferedWriter — wrap OutputStreamWriter for efficient text writing
  • FileReader — the file-path shortcut that builds on InputStreamReader
  • NIO.2: Path & Files — modern alternative with cleaner charset support
  • Scanner — higher-level text parsing that also wraps any InputStream
Last updated June 13, 2026
Was this helpful?