Concepts / Reading and Writing Files with Subclasses of InputStream and OutputStream

Reading and Writing Files with Subclasses of InputStream and OutputStream

Byte streams (InputStream and OutputStream) move binary data without translation, while character streams (Reader and Writer) translate between internal Unicode values and readable character encodings.

  • Programming

Why Programs Send Data as a Stream

When a Java program sends data to a file, a network connection, or another destination, it usually does not move that data as one big object. It moves a stream of data, one piece after another. Java's standard input and output library organizes these streams into two families. One family handles raw binary data, and the other handles human-readable characters.

The central question is practical: when a value leaves your program, does it need to be translated into something a person can read? The answer determines which family of classes you use. Understanding why the two families exist, and which base class belongs to which family, is the foundation for everything else in Java's I/O system.

Two Families: Raw Bytes and Readable Characters

Definition

A byte stream moves binary data without translation. An object that outputs to a byte stream belongs to a subclass of the abstract class OutputStream, and an object that reads from a byte stream belongs to a subclass of the abstract class InputStream. Because nothing is translated, the bits used to represent the data inside the computer are simply copied to and from the stream.

Definition

A character stream moves human-readable character data. Its main classes are the abstract classes Reader and Writer, and every character stream class is a subclass of one of them. The key difference is translation: a Writer converts the program's internal form of a value into a sequence of characters a person could read, and a Reader performs the reverse conversion.

Program valueinternal formOutputStreambyte familyBits copied unchangedno translationWritercharacter familyConversion tocharactersfor example Unicode toASCIIEncoded file bytesreadable charactersbinary datapass-throughreadable datatranslatestored as
When a Reader or Writer is used, where does translation between Unicode characters and encoded bytes happen, and how does that differ from the raw pass-through of an InputStream or OutputStream?

Even plain text involves translation. Inside the computer, characters are stored as 16-bit Unicode values, while character data for English alphabets is generally stored in files using ASCII, which uses only 8 bits per character. Reader and Writer perform this conversion for you, and they can also handle non-western alphabets and characters in non-alphabetic written languages such as Chinese.

Key point

A useful test is to ask whether a person would ever read the stored result directly. Binary data is meant for the program and is copied without change. Character data is meant to be readable, so the library performs the conversion between the program's internal form and the characters that represent it.

The Four Basic I/O Classes

Java provides four basic I/O classes: Reader, Writer, InputStream, and OutputStream. Each is abstract and provides only very primitive I/O operations. Reader and Writer belong to the character family, while InputStream and OutputStream belong to the byte family. The comparison below shows each class under the family it belongs to.

InputStreamreads bytesReaderreads charactersOutputStreamwrites bytesWriterwrites characters
How do InputStream, OutputStream, Reader, and Writer relate to each other, and which family handles bytes versus characters?

Because these base classes are primitive, you will rarely create an object of type InputStream or OutputStream directly. Their job is to define the basic operations that more specialized subclasses build on. Those subclasses add convenient input and output methods to the primitive capabilities of their parents.

Tracing read() Through a Stream

The InputStream class declares an abstract instance method for reading one byte of data from an input stream. Each call to read() delivers the next byte in the stream, and the stream advances so the next call delivers the byte after it. Picture the stream as a line of bytes with a read position that moves forward one step per call.

Call 1a byte from 0 to 255Call 2the next byteCall 3the next byteCall Nthe last byteCall N plus 1returns -1advanceadvanceadvanceend of stream
In what order does read() return bytes from a stream, and at which call does it return -1 to signal that the end of the stream has been reached?

What do you think happens?

After every byte in a stream has already been read, what value does the next call to read() return?

  • 0

  • -1

  • 255

  • An IOException is always thrown

Reveal answer
Answer:

-1

When the end of the input stream is reached, read() returns -1 instead of a byte. Reaching the end is a normal condition, not an error, so it is signaled by this sentinel value.

Because -1 marks the end, a program that consumes a whole stream must check the returned value on every call. The loop below is a generated illustration of that pattern. The name in is assumed to be an InputStream already obtained from some source, and process is a placeholder for whatever the program does with each byte.

java
Notes on the code
  • The first call reads the first byte, or -1 if the stream is empty.
  • The loop continues only while the returned value is not -1.
  • The second read inside the loop advances the stream to the next byte.

Why read() Returns an int Instead of a byte

The result of read() is a number in the range 0 to 255, representing one byte. Notice that the return type is int, not byte. That choice is what makes the valid range 0 to 255 rather than a signed range, and it is why -1 can serve as a sentinel. A real byte can never be -1 in this scheme, so the end-of-stream signal cannot be confused with data.

The same convention governs writing. OutputStream defines a primitive output method for writing one byte, declared as public void write(int b) throws IOException. The int argument supplies a value that is written as a single byte, so values from 0 to 255 are the ones that correspond to one byte of data. Only the low-order eight bits of the argument form the byte that is written.

Why I/O Calls Need try or throws

IOException is a checked exception, which means the compiler requires you to deal with it. You can call read() only inside a try statement, or inside a subroutine that is itself declared with a throws IOException clause. The same requirement applies to write(int b) of OutputStream, which throws IOException if an error occurs during output.

This rule is not a special case for reading. It is the general mandatory handling that applies to every checked exception. The compiler insists on it because an input or output attempt can fail for reasons outside the program's control, and the program must decide in advance what happens then: either handle the failure where it occurs with a try statement, or pass responsibility to the caller by declaring throws IOException.

Mistakes Beginners Make with Streams

  • Common mistake

    Treating -1 as a byte value

    Why it fails

    read() returns an int. The value -1 is the end-of-stream signal, not data, so it must be tested for and not stored as if it were content.

    Do this instead

    Test the returned value against -1 before using it as a byte.

  • Common mistake

    Expecting a byte stream to produce readable text

    What it looks like

    Writing numbers to an OutputStream and then opening the file in a text viewer expecting the digits to appear.

    Why it fails

    Byte streams copy bits without translation, so the output is not meant for direct reading.

    Do this instead

    Use a character stream such as a Writer when the output must be readable, or read the bytes back through an InputStream.

  • Common mistake

    Creating InputStream or OutputStream objects directly and expecting rich methods

    Why it fails

    These base classes are abstract and provide only primitive operations. Convenient methods for types such as int or double come from subclasses.

    Do this instead

    Use the specialized subclasses that add the convenience methods you need.

  • Common mistake

    Ignoring IOException because the code appears to work

    Why it fails

    IOException is checked, so the compiler will reject the code. Handling it is mandatory, not optional.

Practice Tracing a Byte Stream

EASY

A byte stream contains three bytes. Trace the sequence of values returned by four consecutive calls to read(). State the value of each call and explain which call is the first to signal the end of the stream.

Hints
  • The first three calls return the three bytes, each in the range 0 to 255.
  • The fourth call finds no more data.
  • Recall the value that signals end of stream.
MEDIUM

Explain, in your own words, why a program that writes numbers with an OutputStream cannot simply open the resulting file in a text editor and read the numbers, while a program that writes the same values through a Writer can produce readable output.

Hints
  • Think about whether translation happens in each case.
  • Consider whether the library converts values into characters that represent them.

Summary of Byte and Character Streams

  1. 1
    Byte streams (InputStream and OutputStream) move binary data without translation, while character streams (Reader and Writer) translate between internal Unicode values and readable character encodings.
  2. 2
    read() returns one byte in the range 0 to 255, or -1 at the end of the stream. write(int b) writes one byte from its int argument.
  3. 3
    IOException is a checked exception, so I/O calls must appear inside a try statement or in a method declaring throws IOException.
  4. 4
    The four basic I/O classes are abstract and provide only primitive operations. Their subclasses add convenient methods.

At a glance

  • Byte streams copy binary data unchanged, while character streams translate between internal Unicode values and readable encoded characters.
  • Reader and Writer belong to the character family; InputStream and OutputStream belong to the byte family.
  • read() returns an int from 0 to 255 for each byte, and -1 when the end of the stream is reached.
  • write(int b) writes one byte, and IOException is checked, so I/O calls need a try statement or a throws declaration.