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.
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
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.
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.
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.
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.
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.
What do you think happens?
After every byte in a stream has already been read, what value does the next call to read() return?
Reveal 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.
- 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 failsread()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 insteadTest the returned value against -1 before using it as a byte.
Common mistake Expecting a byte stream to produce readable text
What it looks likeWriting numbers to an OutputStream and then opening the file in a text viewer expecting the digits to appear.
Why it failsByte streams copy bits without translation, so the output is not meant for direct reading.
Do this insteadUse 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 failsThese base classes are abstract and provide only primitive operations. Convenient methods for types such as int or double come from subclasses.
Do this insteadUse the specialized subclasses that add the convenience methods you need.
Common mistake Ignoring IOException because the code appears to work
Why it failsIOException is checked, so the compiler will reject the code. Handling it is mandatory, not optional.
Practice Tracing a Byte Stream
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.
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
- 1Byte streams (
InputStreamandOutputStream) move binary data without translation, while character streams (Reader and Writer) translate between internal Unicode values and readable character encodings. - 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. - 3IOException is a checked exception, so I/O calls must appear inside a try statement or in a method declaring throws IOException.
- 4The 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;
InputStreamandOutputStreambelong 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.