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:
| Class | Direction | Wraps |
|---|---|---|
InputStreamReader | bytes → chars (read) | any InputStream |
OutputStreamWriter | chars → 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 — FileInputStream → InputStreamReader → BufferedReader — 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.InputStreamReadershines when you already have anInputStream(for example, from a network connection orProcess.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\non Windows and\non 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 Constant | Canonical Name | Use Case |
|---|---|---|
StandardCharsets.UTF_8 | UTF-8 | Universal default — use this unless you have a reason not to |
StandardCharsets.ISO_8859_1 | ISO-8859-1 | Legacy Latin-1 files, some HTTP responses |
StandardCharsets.US_ASCII | US-ASCII | Configuration files, pure ASCII protocols |
StandardCharsets.UTF_16 | UTF-16 | Windows 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.).
| Feature | FileReader | InputStreamReader |
|---|---|---|
| Opens a file by path | Yes | No (wraps existing stream) |
Accepts any InputStream | No | Yes |
| Charset constructor (Java 11+) | Yes | Yes (all versions) |
| Use when | Reading a local file | Wrapping any byte stream |
Related Topics
- Byte vs Character Streams — understand why the bridge classes exist in the first place
- BufferedReader — wrap
InputStreamReaderfor efficient line-by-line reading - BufferedWriter — wrap
OutputStreamWriterfor 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