Java Byte Streams and Character Streams Explained

Choose InputStream and OutputStream for binary data, Reader and Writer for text, and an explicit charset when converting between them.

Lesson 71 of 127Intermediate13 min read

Byte streams read and write binary data through InputStream and OutputStream. Character streams read and write text through Reader and Writer. The choice depends on what the data means: an image needs its exact bytes preserved, while a text document needs those bytes decoded using the correct character encoding.

Java connects the two families with InputStreamReader and OutputStreamWriter. These adapters use a charset to turn bytes into characters or characters into bytes. Understanding that boundary prevents corrupted text and avoids accidentally treating binary content as a document.

A byte is not automatically a character

A byte carries eight bits. A file format decides what those bits represent: an image pixel, part of a compressed archive, a number, or part of an encoded character. InputStream does not interpret their meaning.

A character encoding defines how text becomes bytes. ASCII characters use one byte in UTF-8, but many accented letters use two and other characters use more. Therefore the number of bytes in a text file is not necessarily its number of letters.

Java's char represents a UTF-16 code unit. Some Unicode characters require two such units, so a Reader's character count is also not always a count of user-perceived characters. This distinction is important when measuring file size, validating text length, or positioning a cursor.

For copying an image, stay at the byte level. For searching words in a document, decode the bytes first and work with text. Applying a text decoder to arbitrary image bytes can lose information that cannot be recovered by writing the resulting characters back.

End markers, buffers and partial reads

InputStream.read returns minus one when no more data remains. A result of zero from the single-byte method is a real zero-valued byte. It is not a signal to stop.

The array overload returns the number of bytes placed in the buffer, or minus one at the end. It can return fewer bytes than the buffer's capacity without reaching the final file position. Process only that returned count; unused buffer positions may contain leftovers from an earlier read.

OutputStream.write(buffer, offset, count) is the matching operation for transferring exactly the newly read portion. Writing the entire buffer after a short final read can append stale bytes and corrupt a copy.

Do not use available as a file-length calculator or a reliable end-of-stream test. It estimates bytes readable without blocking for that stream, which is a different question from how much content ultimately exists.

Reader and Writer are text abstractions

Reader provides single-character and character-array reads. Writer supplies corresponding write operations. These APIs let a text algorithm work without knowing whether its source is a file, network adapter, string or in-memory test fixture.

StringReader and StringWriter are useful for tests that should focus on parsing or formatting rather than disk access. InputStreamReader and OutputStreamWriter sit at the conversion boundary when the underlying source or destination carries bytes.

A Reader does not automatically split text into lines or parse CSV fields. BufferedReader adds convenient line reading, while a format parser handles delimiters and quoting. Keep these layers distinct so changing the storage location does not require rewriting the document's business rules.

Buffering, flushing and ownership

Buffering batches small operations to reduce calls to the underlying destination. Flush asks a buffered writer or stream to forward pending content. It does not generally promise that a storage device has made the data durable against a power failure.

Closing a wrapper normally closes its underlying stream. Establish who owns the resource before adding try-with-resources inside a helper. A method that receives a caller-owned Writer may be expected to write without closing it, so the caller can continue producing the document.

For resources created locally, try-with-resources is the clearest default. If an I/O operation fails, propagate or handle IOException with useful context. Announcing a successful export after catching and ignoring that error hides a real failure.

For malformed text that must be rejected instead of replaced, configure a CharsetDecoder with an explicit error policy. The adapter's ordinary behavior is not a substitute for a strict file-format validation contract.

Byte and Character Streams Summary

  • Choose the stream family according to the data's meaning.
  • Specify a charset when converting text and bytes.
  • Preserve the int end marker until it has been tested.
  • Process only the count returned by a buffered read.
  • Apply these rules in file streams.

Byte and Character Streams Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Reading binary values without losing 255

Save this program as BinaryReadExample.java.

Do not cast the result to byte before testing for end-of-stream. That would turn a genuine value of 255 into minus one and make it indistinguishable from the read method's end marker. The int is part of the API contract, not unnecessary extra storage.

Java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;

public class BinaryReadExample {
    public static void main(String[] args) throws IOException {
        byte[] data = {0, 127, (byte) 255};
        try (InputStream input = new ByteArrayInputStream(data)) {
            int value;
            while ((value = input.read()) != -1) {
                System.out.println(value);
            }
        }
    }
}
Output
0
127
255
Line-by-line explanation
  1. Line 7The array stores three eight-bit patterns. Java's signed byte type represents the last pattern as minus one.
  2. Lines 9 to 10read returns an int so it can distinguish a byte value from its separate end marker.
  3. Line 11A successfully read byte is returned between zero and 255, even when the original byte variable would be negative.
Example 2

Decoding UTF-8 bytes into text

An explicit UTF-8 reader handles the accented letter as part of the document.

Choosing the wrong charset may produce a plausible-looking word with incorrect characters. File extensions do not reliably identify the encoding. Use a documented format or supplied encoding rather than guessing from the machine's default.

Java
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class DecodedTextExample {
    public static void main(String[] args) throws IOException {
        byte[] encoded = "Café".getBytes(StandardCharsets.UTF_8);
        StringBuilder text = new StringBuilder();
        try (Reader reader = new InputStreamReader(
                new ByteArrayInputStream(encoded), StandardCharsets.UTF_8)) {
            int character;
            while ((character = reader.read()) != -1) {
                text.append((char) character);
            }
        }
        System.out.println(encoded.length);
        System.out.println(text);
        System.out.println(text.length());
    }
}
Output
5
Café
4
Line-by-line explanation
  1. Line 9The runtime string becomes five UTF-8 bytes because the accented letter takes two bytes.
  2. Lines 11 to 12The adapter decodes those bytes with the same charset used to create them.
  3. Lines 14 to 15The Reader supplies UTF-16 code units which are appended to the text.
  4. Lines 18 to 20The byte length and String length differ, while the decoded word remains intact.
Example 3

Encoding text through a Writer

This program writes an accented greeting into an in-memory byte destination.

ByteArrayOutputStream permits inspecting its data after close, making this fixture convenient. Do not assume every destination has that contract; a closed file stream cannot simply be reused for another write.

Java
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class EncodedGreetingExample {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (Writer writer = new OutputStreamWriter(bytes, StandardCharsets.UTF_8)) {
            writer.write("Olá");
        }
        System.out.println(bytes.size());
        System.out.println(bytes.toString(StandardCharsets.UTF_8));
    }
}
Output
4
Olá
Line-by-line explanation
  1. Lines 9 to 11The Writer encodes the greeting into the byte destination using UTF-8.
  2. Line 12Closing the adapter flushes its pending encoded content before the bytes are inspected.
  3. Lines 13 to 14Three UTF-16 code units become four bytes and decode back to the original greeting.

Byte and Character Streams FAQ

Is BufferedReader suitable for binary files?

No. It works with decoded text. Use a byte-level API for content whose exact binary representation must be preserved.

Does closing a Writer close its byte destination?

Usually yes for adapters such as OutputStreamWriter. Read the implementation's contract and respect caller ownership.

Are I/O streams the same as the Stream API?

No, despite the shared name. InputStream and OutputStream in java.io move bytes to and from files, networks and memory. java.util.stream.Stream, used with filter and map, processes sequences of objects. Files.lines connects the two by turning a file's lines into a Stream API stream.

Why does InputStream.read() return an int instead of a byte?

It needs one more value than a byte can hold. A byte value is returned as 0 to 255, and -1 is reserved to mean "end of stream". If read() returned a byte, the data value 255 and the end marker would look the same.

What happens if I decode text with the wrong charset?

The bytes are interpreted with the wrong rules, so characters outside plain English come out garbled. The UTF-8 bytes for café decoded as ISO-8859-1 show as café. Decoding can also replace invalid sequences with �. Agree on UTF-8 for text files and name the charset explicitly when reading.

Byte and Character Streams Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What should copy a PNG file without interpreting its content?

Predict the outputmedium

UTF-8 encodes Café into five bytes. Does its Java String length have to be five?

Byte and Character Streams Quiz

2 questions to check what you learned.

  1. Why does InputStream.read return int?

    • To represent byte values zero through 255 and a separate minus-one end marker
    • Because a file cannot contain zero
    • Because it always reads four bytes
  2. A buffered read returns three bytes into an eight-byte buffer. How many should be written?

    • Three
    • Eight
    • Zero until the buffer is full