Byte vs Character Streams
Java’s I/O system is split into two parallel families: byte streams that move raw 8-bit data, and character streams that move 16-bit Unicode text. Choosing the right family up front saves you from garbled characters, corrupted binary files, and confusing bugs.
The Two Families at a Glance
| Feature | Byte Streams | Character Streams |
|---|---|---|
| Base abstract classes | InputStream / OutputStream | Reader / Writer |
| Unit of transfer | 1 byte (8 bits) | 1 char (16 bits, UTF-16) |
| Typical use | Images, audio, ZIP, binary protocols | .txt, .csv, source code, JSON |
| Encoding awareness | None | Yes — honours the charset |
| Package | java.io | java.io |
Both families live in java.io and share the same open/read/write/close lifecycle.
Byte Streams
Every byte stream class ultimately extends either InputStream or OutputStream. The key operations are read() (returns an int where –1 signals end-of-stream) and write(int b).
Reading bytes from a file
import java.io.FileInputStream;
import java.io.IOException;
public class ByteReadDemo {
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("photo.jpg")) {
int byteValue;
int count = 0;
while ((byteValue = fis.read()) != -1) {
count++;
}
System.out.println("Total bytes read: " + count);
}
}
}
Output:
Total bytes read: 204800 // will vary by file size
Writing bytes to a file
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteWriteDemo {
public static void main(String[] args) throws IOException {
byte[] data = {72, 101, 108, 108, 111}; // ASCII "Hello"
try (FileOutputStream fos = new FileOutputStream("output.bin")) {
fos.write(data);
}
System.out.println("File written successfully.");
}
}
Tip: Always wrap raw byte streams with a
BufferedInputStream/BufferedOutputStreamfor real-world I/O. Buffering reduces system calls from O(n) down to O(n/bufferSize), which can be orders of magnitude faster. See BufferedInputStream.
Important byte-stream subclasses
| Class | Purpose |
|---|---|
FileInputStream | Read bytes from a file |
FileOutputStream | Write bytes to a file |
BufferedInputStream | Buffered reading for speed |
BufferedOutputStream | Buffered writing for speed |
DataInputStream | Read Java primitives (int, double…) |
DataOutputStream | Write Java primitives |
ObjectInputStream | Deserialize objects |
ObjectOutputStream | Serialize objects |
Character Streams
Every character-stream class ultimately extends Reader or Writer. They decode/encode bytes to chars automatically using a specified charset (defaults to the platform default if none is given — which is often a source of bugs on systems with different locales).
Reading text from a file
import java.io.FileReader;
import java.io.IOException;
public class CharReadDemo {
public static void main(String[] args) throws IOException {
try (FileReader fr = new FileReader("hello.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
}
}
}
Output:
Hello, World!
Writing text to a file
import java.io.FileWriter;
import java.io.IOException;
public class CharWriteDemo {
public static void main(String[] args) throws IOException {
try (FileWriter fw = new FileWriter("greeting.txt")) {
fw.write("Namaste, Java!\n");
fw.write("Character streams handle Unicode automatically.");
}
System.out.println("Text written successfully.");
}
}
Tip: Just like byte streams, wrap
FileReader/FileWriterwithBufferedReader/BufferedWriterfor line-by-line reading withreadLine(). See BufferedReader.
Important character-stream subclasses
| Class | Purpose |
|---|---|
FileReader | Read chars from a file |
FileWriter | Write chars to a file |
BufferedReader | Buffered reading + readLine() |
BufferedWriter | Buffered writing + newLine() |
InputStreamReader | Bridge: byte stream → char stream |
OutputStreamWriter | Bridge: char stream → byte stream |
PrintWriter | Formatted text output |
StringReader / StringWriter | In-memory char I/O |
Bridging the Two Worlds
Sometimes you have a byte stream (e.g., from a network socket) but you need to work with it as text. The bridge classes InputStreamReader and OutputStreamWriter handle this conversion, and you can explicitly specify the charset:
import java.io.*;
import java.nio.charset.StandardCharsets;
public class BridgeDemo {
public static void main(String[] args) throws IOException {
// Read a UTF-8 encoded file through a byte stream, treat as chars
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
Warning: Never use
FileReaderwhen you need a specific encoding.FileReaderuses the platform default charset, which varies between operating systems. UseInputStreamReaderwith an explicitStandardCharsets.UTF_8(or another charset) instead for portable code.
Choosing the Right Stream
Ask yourself one question: “Is this data text or binary?”
- Text (human-readable): Use character streams (
Reader/Writerhierarchy). This covers.txt,.csv,.json,.xml,.html,.java, and any other format meant to be read as characters. - Binary (machine data): Use byte streams (
InputStream/OutputStreamhierarchy). This covers images (.jpg,.png), audio (.mp3), video, compiled.classfiles, ZIP archives, and custom binary protocols.
Note: Reading an image file with a
FileReaderwill silently corrupt it because the reader applies charset decoding to raw bytes that were never encoded text. Stick to byte streams for binary data.
Under the Hood
How Java models streams internally
Both InputStream and Reader are abstract classes, not interfaces. Each concrete implementation (e.g., FileInputStream) overrides the single abstract method read() and may override the bulk read(byte[] buf, int off, int len) for efficiency. The decorator pattern is used throughout: BufferedInputStream wraps any InputStream and adds a byte array buffer (default 8 KB) so that the underlying read() is called far less often.
Character encoding and the JVM
The JVM stores all char and String values internally as UTF-16. When InputStreamReader converts bytes to chars, it uses a CharsetDecoder object under the hood. For the UTF-8 charset, multi-byte sequences (2–4 bytes per code point) are decoded into one or two UTF-16 char values (a surrogate pair for code points above U+FFFF). This is why you should always specify the charset explicitly rather than relying on Charset.defaultCharset().
Performance notes
- A single
fis.read()call triggers a native OS call each time — O(1) per byte is fine for small files but catastrophic for large ones. Wrapping withBufferedInputStreambatches reads into 8 KB chunks. BufferedReader.readLine()scans the internal buffer for\nor\r\nwithout extra allocations per character, making it significantly faster than reading char-by-char.- In Java 11+,
Files.readString(Path)andFiles.writeString(Path, CharSequence)handle the open/buffer/close boilerplate for small text files in a single line. They use UTF-8 by default.
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
public class ModernFileReadDemo {
public static void main(String[] args) throws IOException {
// Java 11+ one-liner (UTF-8 by default)
String content = Files.readString(Path.of("greeting.txt"));
System.out.println(content);
}
}
Output:
Namaste, Java!
Character streams handle Unicode automatically.
Note:
Files.readString()is great for small files but loads the entire content into memory. For large files (logs, CSVs with millions of rows), preferFiles.lines(Path)which returns a lazyStream<String>, or aBufferedReaderin a loop. See Stream API.
Quick Decision Checklist
- Is your data text? →
Reader/Writer - Do you need a specific charset? →
InputStreamReaderwith an explicitCharset - Are you copying files or working with raw bytes? →
InputStream/OutputStream - Do you need to serialize Java objects? →
ObjectOutputStream(byte stream) - Do you need fast line-by-line reading? →
BufferedReader.readLine() - Are you on Java 11+ with a small text file? →
Files.readString()/Files.writeString()
Related Topics
- Java I/O Overview — the big picture of all I/O classes and when to use them
- BufferedReader — efficient line-by-line text reading with
readLine() - FileInputStream — reading raw bytes from files in detail
- FileWriter — writing text to files with the character stream API
- InputStreamReader / OutputStreamWriter — the bridge classes explained in depth
- Serialization — using object byte streams to persist Java objects