Concepts / Byte Streams and Binary Data in Java

Byte Streams and Binary Data in Java

InputStreamReader and OutputStreamWriter wrap an existing byte stream so that a program can read and write characters instead of raw bytes.

  • Programming

Why Programs Need Characters Instead of Bytes

Java's byte streams move raw binary data, one byte at a time. That is a good fit for images, compressed archives, and other data whose meaning is not text. Many programs, however, care about text: names, chat messages, and the lines of a document. A program that handles text wants to work with characters, not with the raw numbers that happen to represent them.

The question this article answers is how a byte stream can be connected to a character stream, so that text can be read from and written to a source that only understands bytes. The short answer is that you wrap the byte stream. You hand an existing byte stream to a constructor and receive back a character stream that performs the conversion for you. The program then reads and writes characters, and the wrapper handles the bytes underneath.

Wrapping a Byte Stream

InputStreamReader and OutputStreamWriter are the two wrappers that bridge the gap. An InputStreamReader wraps an InputStream so that the program reads characters from it. An OutputStreamWriter wraps an OutputStream so that the program writes characters into it. In both cases the wrapper is the only place where the conversion between characters and bytes happens. The byte stream itself still moves bytes, and the program does not touch those bytes directly once the wrapper is in place.

java
Output
Byte count: 5
Decoded: café
Notes on the code
  • ByteArrayOutputStream is a byte stream that collects bytes in memory
  • OutputStreamWriter turns the characters written into it into bytes before they reach the byte stream
  • ByteArrayInputStream is a byte stream that supplies the stored bytes
  • InputStreamReader turns those bytes back into characters for the program

Read the program line by line and notice what it never does. It never calls a method that handles individual bytes when it writes or reads the text. It calls write with a String and read to receive characters. The byte values appear only in the stored array, which the program prints as a length. The count of 5 is a clue: the four characters of café took five bytes to store, because the letter é needed more than one byte under UTF-8. The charset section below explains why.

Tracing a Read Through the Wrappers

When data is read, the flow runs from the byte source upward. Bytes come out of the byte stream, the InputStreamReader turns them into characters, and your code receives characters. The diagram below traces that path for a single read. Follow the arrows and note at which step the value stops being a byte and becomes a char.

Byte sourceByteArrayInputStreamRaw bytesC3 A9 for é in UTF-8InputStreamReaderdecodes with a charsetchar valueé as one charProgram codereceives char from read()delivers bytesconsumed byproducesreturned by read
When a program reads from an InputStream through an InputStreamReader, what happens to the raw bytes at each step until the program gets a char?

Writing runs the other way. The program supplies characters, the OutputStreamWriter turns them into bytes, and the byte stream delivers those bytes to its destination. In both directions the wrapper sits between the program and the byte stream, and the program has no direct handle on the bytes once they are wrapped. Everything it sees passes through the character stream.

Best practice

Create the byte stream first, then build the wrapper around it. The wrapper needs something to read from or write to, so the byte stream must exist before the wrapper is constructed.

Reading Standard Input as Text

System.in is the standard input stream, and its declared type is InputStream. That makes it a byte stream, which is an unusual arrangement for something a program reads from the keyboard as text. Because of that, it is common to wrap System.in in a Reader when you want to read character data from standard input. The wrapping is a single line of code.

java
Output
(If you type a and press Enter, the first value read is 97, the code for the letter a.)
Notes on the code
  • System.in is passed in as the byte stream to wrap
  • read returns an int holding the character value, or -1 at end of input

The same pattern applies to network connections. The input and output streams tied to a network connection are byte streams, and wrapping them lets a program send and receive character data over the network without handling bytes directly. Network input and output is treated in a later section of the course, so this article focuses on the wrapping itself and on the charset that the wrapper relies on.

What a Charset Decides

Definition

A charset is a named encoding that decides how characters map to binary data. Given a character, the charset says which bytes represent it. Given bytes, the charset says which characters they stand for. UTF-16, UTF-8, and ISO-8859-1 are three examples of charsets.

The bytes produced by a writer are only meaningful if a reader interprets them with the same charset. The charset is therefore the missing piece in any wrapped stream. It is not a detail to ignore when data crosses from one program or machine to another, because the bytes alone do not say which characters they were meant to be.

Key point

The charset decides the size of each stored character. One character does not always equal one byte. A program that assumes it does will be wrong under some charsets.

Comparing UTF-8, UTF-16, and ISO-8859-1

The three charsets store the same character in different amounts of space. The diagram groups each charset into a column and shows the unit or units that represent one character. Read the columns from left to right to see how the amount of storage changes with the encoding. The letter é is a useful test case because it is a plain Latin letter with an accent, which every one of these charsets can represent.

UTF-8C3 A9UTF-1600 E9 (big-endian)ISO-8859-1E9
How does the same character, such as é, map to different byte sequences under UTF-8, UTF-16, and ISO-8859-1?

The three columns show the same character taking different amounts of storage. Under ISO-8859-1 the letter é is one byte. Under UTF-8 it takes two bytes, which is why the earlier café example stored five bytes for four characters. Under UTF-16 the letter is also written in a two-byte unit, shown here in big-endian order. The lesson is not to memorize these byte values, but to see that the charset, not the character, determines how much space it takes.

CharsetExample useStorage for éKey lesson
UTF-8Text shared between programs and machinesTwo bytesCharacters can take a varying number of bytes
UTF-16Java strings in memoryTwo-byte unitEach common character uses a fixed two-byte unit
ISO-8859-1Western European single-byte textOne byteOne byte per character, but limited to its own repertoire

Three charsets compared on how they store the same character

Choosing a Charset Explicitly

Readers and Writers use the default charset of the computer they run on unless you choose otherwise. You can name a different charset in the constructor. The wrapper in the demo passed the name of the charset as a second argument, which overrides the default for that stream only. The same two-argument form works for both wrappers.

java
Notes on the code
  • The first argument is the byte stream to wrap
  • The second argument names the charset used for this stream only
  • StandardCharsets.UTF_8 is a predefined charset constant

Specifying the charset is a choice you make at the point where the stream is created. Once the wrapper exists, it converts characters to bytes, or bytes to characters, using that charset. The choice is fixed for that stream, so you make it when you build the wrapper rather than later.

Default Charset Versus Explicit Charset

If you do not pass a charset, the wrapper uses the default charset of the machine the code runs on. The result then depends on that machine. Code that writes bytes on one computer and reads them on another can produce different text if the two defaults differ. This is the reason to name the charset whenever the bytes will be read somewhere else.

Locating a Charset Mismatch

A mismatch appears when the bytes were written with one charset and read with another. The reader does not know the bytes came from a different charset, so it interprets each byte using its own rules. The wrong text appears on the reading side, at the point where the reader's wrapper converts bytes to characters. The writing side may have produced perfectly correct bytes.

What do you think happens?

The text café is written with UTF-8 and read back with ISO-8859-1. What text does the reader produce?

Reveal answer
Answer:

café

UTF-8 stores é as the two bytes C3 A9. ISO-8859-1 maps C3 to à and A9 to ©, so the reader sees four characters that look like café. The bytes were fine; only the reading charset was wrong.

café as UTF-8written bytesC3 A9the two bytes for éécorrect read with UTF-8café as UTF-8same written bytesC3 A9read as two ISO-8859-1bytesà and ©wrong text: caféstored asdecoded by UTF-8stored asdecoded by ISO-8859-1
If text is written as UTF-8 but read back as ISO-8859-1, which exact bytes get reinterpreted, and where does the wrong output appear?
java
Output
café
Notes on the code
  • The write side uses UTF-8, so é becomes two bytes
  • The read side names a different charset, so the same two bytes become two characters
  • The mismatch is at the reader's constructor, not in the write call

Mistakes Beginners Make with Charsets

  • Common mistake

    Assuming one character always equals one byte

    What it looks like

    Storing café as UTF-8 and assuming the stored length is 4

    Why it fails

    The charset decides how many bytes each character takes, and UTF-8 used two bytes for é

    Do this instead

    Measure the bytes after encoding rather than counting characters

  • Common mistake

    Writing with one charset and reading with another

    What it looks like

    Writer uses UTF-8, reader uses ISO-8859-1, and the output shows café

    Why it fails

    The reader interprets the bytes with rules that did not produce them

    Do this instead

    Use the same charset constant on both the writing and reading wrappers

  • Common mistake

    Leaving out the charset and trusting the default

    What it looks like

    new InputStreamReader(in) on a machine with a different default than the writer

    Why it fails

    The default depends on the machine, so the bytes may decode differently elsewhere

    Do this instead

    Name the charset explicitly whenever the bytes leave the current machine

  • Common mistake

    Building the wrapper before the byte stream exists

    Why it fails

    The wrapper needs a byte stream to read from or write to

    Do this instead

    Create the byte stream first and pass it to the wrapper constructor

Practice: Trace the Round Trip

MEDIUM

Write the word naïve using UTF-8 with an OutputStreamWriter into a ByteArrayOutputStream. Then read the stored bytes back using an InputStreamReader with UTF-8. Before running the code, predict the byte count and the decoded text. Next, change only the reader to ISO-8859-1 and predict what text appears. Explain at which line the mismatch happens and why the write side was not at fault.

Hints
  • The letter ï is an accented letter like é, so think about how many bytes UTF-8 uses for it
  • Only the reader's charset argument changes in the second run
  • The bytes are created by the writer, so the mismatch is in the reading wrapper

Key Takeaways

  • InputStreamReader and OutputStreamWriter wrap an existing byte stream so the program reads and writes characters while the wrapper performs the conversion to and from bytes.
  • On read, bytes flow from the byte source through the InputStreamReader into characters; on write, characters flow through the OutputStreamWriter into bytes.
  • A charset is a named encoding such as UTF-8, UTF-16, or ISO-8859-1, and it decides how many bytes each character occupies.
  • Name the charset explicitly in the constructor, because the default depends on the machine and a mismatch between writing and reading produces wrong text on the reading side.