Skip to content
Java io 6 min read

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

FeatureByte StreamsCharacter Streams
Base abstract classesInputStream / OutputStreamReader / Writer
Unit of transfer1 byte (8 bits)1 char (16 bits, UTF-16)
Typical useImages, audio, ZIP, binary protocols.txt, .csv, source code, JSON
Encoding awarenessNoneYes — honours the charset
Packagejava.iojava.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 / BufferedOutputStream for 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

ClassPurpose
FileInputStreamRead bytes from a file
FileOutputStreamWrite bytes to a file
BufferedInputStreamBuffered reading for speed
BufferedOutputStreamBuffered writing for speed
DataInputStreamRead Java primitives (int, double…)
DataOutputStreamWrite Java primitives
ObjectInputStreamDeserialize objects
ObjectOutputStreamSerialize 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 / FileWriter with BufferedReader / BufferedWriter for line-by-line reading with readLine(). See BufferedReader.

Important character-stream subclasses

ClassPurpose
FileReaderRead chars from a file
FileWriterWrite chars to a file
BufferedReaderBuffered reading + readLine()
BufferedWriterBuffered writing + newLine()
InputStreamReaderBridge: byte stream → char stream
OutputStreamWriterBridge: char stream → byte stream
PrintWriterFormatted text output
StringReader / StringWriterIn-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 FileReader when you need a specific encoding. FileReader uses the platform default charset, which varies between operating systems. Use InputStreamReader with an explicit StandardCharsets.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 / Writer hierarchy). 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 / OutputStream hierarchy). This covers images (.jpg, .png), audio (.mp3), video, compiled .class files, ZIP archives, and custom binary protocols.

Note: Reading an image file with a FileReader will 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 with BufferedInputStream batches reads into 8 KB chunks.
  • BufferedReader.readLine() scans the internal buffer for \n or \r\n without extra allocations per character, making it significantly faster than reading char-by-char.
  • In Java 11+, Files.readString(Path) and Files.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), prefer Files.lines(Path) which returns a lazy Stream<String>, or a BufferedReader in a loop. See Stream API.

Quick Decision Checklist

  • Is your data text? → Reader / Writer
  • Do you need a specific charset? → InputStreamReader with an explicit Charset
  • 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()
Last updated June 13, 2026
Was this helpful?