[ Next Section | Chapter Index | Main Index ]

Section 11.1

I/O Streams, Readers, and Writers


Without the ability to interact with the rest of the world, a program would be useless. The interaction of a program with the rest of the world is referred to as input/output or I/O. Historically, one of the hardest parts of programming language design has been coming up with good facilities for doing input and output. A computer can be connected to many different types of input and output devices. If a programming language had to deal with each type of device as a special case, the complexity would be overwhelming. One of the major achievements in the history of programming has been to come up with good abstractions for representing I/O devices. In Java, the main I/O abstractions are called I/O streams. Other I/O abstractions, such as "files" and "channels" also exist, but in this section we will look only at streams. Every stream represents either a source of input or a destination to which output can be sent.


11.1.1  Character and Byte Streams

When dealing with input/output, you have to keep in mind that there are two broad categories of data: machine-formatted data and human-readable text. Machine-formatted data is represented in binary form, the same way that data is represented inside the computer, that is, as strings of zeros and ones. Human-readable data is in the form of characters. When you read a number such as 3.141592654, you are reading a sequence of characters and interpreting them as a number. The same number would be represented in the computer as a bit-string that you would find unrecognizable.

To deal with the two broad categories of data representation, Java has two broad categories of streams: byte streams for machine-formatted data and character streams for human-readable data. There are several predefined classes that represent streams of each type.

An object that outputs data to a byte stream belongs to one of the subclasses of the abstract class OutputStream. Objects that read data from a byte stream belong to subclasses of the abstract class InputStream. If you write numbers to an OutputStream, you won't be able to read the resulting data yourself. But the data can be read back into the computer with an InputStream. The writing and reading of the data will be very efficient, since there is no translation involved: the bits that are used to represent the data inside the computer are simply copied to and from the streams.

For reading and writing human-readable character data, the main classes are the abstract classes Reader and Writer. All character stream classes are subclasses of one of these. If a number is to be written to a Writer stream, the computer must translate it into a human-readable sequence of characters that represents that number. Reading a number from a Reader stream into a numeric variable also involves a translation, from a character sequence into the appropriate bit string. (Even if the data you are working with consists of characters in the first place, such as words from a text editor, there might still be some translation. Characters are stored in the computer as 16-bit Unicode values. For people who use the English alphabets, character data is generally stored in files in ASCII code, which uses only 8 bits per character. The Reader and Writer classes take care of this translation, and can also handle non-western alphabets and characters in non-alphabetic written languages such as Chinese.)

Byte streams can be useful for direct machine-to-machine communication, and they can sometimes be useful for storing data in files, especially when large amounts of data need to be stored efficiently, such as in large databases. However, binary data is fragile in the sense that its meaning is not self-evident. When faced with a long series of zeros and ones, you have to know what information it is meant to represent and how that information is encoded before you will be able to interpret it. Of course, the same is true to some extent for character data, since characters, like any other kind of data, have to be coded as binary numbers to be stored or processed by a computer. But the binary encoding of character data has been standardized and is well understood, and data expressed in character form can be made meaningful to human readers. The current trend seems to be towards increased use of character data, represented in a way that will make its meaning as self-evident as possible. We'll look at one way this is done in Section 11.5.

I should note that the original version of Java did not have character streams, and that for ASCII-encoded character data, byte streams are largely interchangeable with character streams. In fact, the standard input and output streams, System.in and System.out, are byte streams rather than character streams. However, you should prefer Readers and Writers rather than InputStreams and OutputStreams when working with character data, even when working with the standard ASCII character set.

The standard I/O stream classes discussed in this section are defined in the package java.io, along with several supporting classes. You must import the classes from this package if you want to use them in your program. That means either importing individual classes or putting the directive "import java.io.*;" at the beginning of your source file. I/O streams are used for working with files and for doing communication over a network. They can also be used for communication between two concurrently running threads, and there are stream classes for reading and writing data stored in the computer's memory.

(Note: The Java API provides additional support for I/O in the package java.nio and its subpackages, but they are not covered in this textbook. In general, java.nio gives programmers efficient access to more advanced I/O techniques.)

The beauty of the stream abstraction is that it is as easy to write data to a file or to send data over a network as it is to print information on the screen.


The basic I/O classes Reader, Writer, InputStream, and OutputStream provide only very primitive I/O operations. For example, the InputStream class declares an abstract instance method

public int read() throws IOException

for reading one byte of data, as a number in the range 0 to 255, from an input stream. If the end of the input stream is encountered, the read() method will return the value -1 instead. If some error occurs during the input attempt, an exception of type IOException is thrown. Since IOException is a checked exception, this means that you can't use the read() method except inside a try statement or in a subroutine that is itself declared with a "throws IOException" clause. (Checked exceptions and mandatory exception handling were covered in Subsection 8.3.3.)

The InputStream class also defines methods for reading multiple bytes of data in one step into an array of bytes, which can be a lot more efficient that reading individual bytes. However, InputStream provides no convenient methods for reading other types of data, such as int or double, from a stream. This is not a problem because you will rarely use an object of type InputStream itself. Instead, you'll use subclasses of InputStream that add more convenient input methods to InputStream's rather primitive capabilities. Similarly, the OutputStream class defines a primitive output method for writing one byte of data to an output stream. The method is defined as:

public void write(int b) throws IOException

The parameter is of type int rather than byte, but the parameter value is type-cast to type byte before it is written; this effectively discards all but the eight low order bits of b. Again, in practice, you will almost always use higher-level output operations defined in some subclass of OutputStream.

The Reader and Writer classes provide the analogous low-level read and write methods. As in the byte stream classes, the parameter of the write(c) method in Writer and the return value of the read() method in Reader are of type int, but in these character-oriented classes, the I/O operations read and write characters rather than bytes. The return value of read() is -1 if the end of the input stream has been reached. Otherwise, the return value must be type-cast to type char to obtain the character that was read. In practice, you will ordinarily use higher level I/O operations provided by sub-classes of Reader and Writer, as discussed below.


11.1.2  PrintWriter

One of the neat things about Java's I/O package is that it lets you add capabilities to a stream by "wrapping" it in another stream object that provides those capabilities. The wrapper object is also a stream, so you can read from or write to it—but you can do so using fancier operations than those available for basic streams.

For example, PrintWriter is a subclass of Writer that provides convenient methods for outputting human-readable character representations of all of Java's basic data types. If you have an object belonging to the Writer class, or any of its subclasses, and you would like to use PrintWriter methods to output data to that Writer, all you have to do is wrap the Writer in a PrintWriter object. You do this by constructing a new PrintWriter object, using the Writer as input to the constructor. For example, if charSink is of type Writer, then you could say

PrintWriter printableCharSink = new PrintWriter(charSink);

In fact, the parameter to the constructor can also be an OutputStream or a File, and the constructor will build a PrintWriter that can write to that output destination. (Files are covered in the next section.) When you output data to the PrintWriter printableCharSink, using the high-level output methods in PrintWriter, that data will go to exactly the same place as data written directly to charSink. You've just provided a better interface to the same output destination. For example, this allows you to use PrintWriter methods to send data to a file or over a network connection.

For the record, if out is a variable of type PrintWriter, then the following methods are defined:

Note that none of these methods will ever throw an IOException. Instead, the PrintWriter class includes the method

public boolean checkError()

which will return true if any error has been encountered while writing to the stream. The PrintWriter class catches any IOExceptions internally, and sets the value of an internal error flag if one occurs. The checkError() method can be used to check the error flag. This allows you to use PrintWriter methods without worrying about catching exceptions. On the other hand, to write a fully robust program, you should call checkError() to test for possible errors whenever you use a PrintWriter.


11.1.3  Data Streams

When you use a PrintWriter to output data to a stream, the data is converted into the sequence of characters that represents the data in human-readable form. Suppose you want to output the data in byte-oriented, machine-formatted form. The java.io package includes a byte-stream class, DataOutputStream, that can be used for writing data values to streams in internal, binary-number format. DataOutputStream bears the same relationship to OutputStream that PrintWriter bears to Writer. That is, whereas OutputStream only has methods for outputting bytes, DataOutputStream has methods writeDouble(double x) for outputting values of type double, writeInt(int x) for outputting values of type int, and so on. Furthermore, you can wrap any OutputStream in a DataOutputStream so that you can use the higher level output methods on it. For example, if byteSink is of type OutputStream, you could say

DataOutputStream dataSink = new DataOutputStream(byteSink);

to wrap byteSink in a DataOutputStream.

For input of machine-readable data, such as that created by writing to a DataOutputStream, java.io provides the class DataInputStream. You can wrap any InputStream in a DataInputStream object to provide it with the ability to read data of various types from the byte-stream. The methods in the DataInputStream for reading binary data are called readDouble(), readInt(), and so on. Data written by a DataOutputStream is guaranteed to be in a format that can be read by a DataInputStream. This is true even if the data stream is created on one type of computer and read on another type of computer. The cross-platform compatibility of binary data is a major aspect of Java's platform independence.

In some circumstances, you might need to read character data from an InputStream or write character data to an OutputStream. This is not a problem, since characters, like all data, are ultimately represented as binary numbers. However, for character data, it is convenient to use Reader and Writer instead of InputStream and OutputStream. To make this possible, you can wrap a byte stream in a character stream. If byteSource is a variable of type InputStream and byteSink is of type OutputStream, then the statements

Reader charSource = new InputStreamReader( byteSource );
Writer charSink   = new OutputStreamWriter( byteSink );

create character streams that can be used to read character data from and write character data to the byte streams. In particular, the standard input stream System.in, which is of type InputStream for historical reasons, can be wrapped in a Reader to make it easier to read character data from standard input:

Reader charIn = new InputStreamReader( System.in );

As another application, the input and output streams that are associated with a network connection are byte streams rather than character streams, but the byte streams can be wrapped in character streams to make it easy to send and receive character data over the network. We will encounter network I/O in Section 11.4.

There are various ways for characters to be encoded as binary data. A particular encoding is known as a charset or character set. Charsets have standardized names such as "UTF-16," "UTF-8," and "ISO-8859-1." In UTF-16, characters are encoded as 16-bit UNICODE values; this is the character set that is used internally by Java. UTF-8 is a way of encoding UNICODE characters using 8 bits for common ASCII characters and longer codes for other characters. ISO-8859-1, also known as "Latin-1," is an 8-bit encoding that includes ASCII characters as well as certain accented characters that are used in several European languages. Readers and Writers use the default charset for the computer on which they are running, unless you specify a different one. That can be done, for example, in a constructor such as

Writer charSink = new OutputStreamWriter( byteSink, "ISO-8859-1" );

Certainly, the existence of a variety of charset encodings has made text processing more complicated—unfortunate for us English-speakers but essential for people who use non-Western character sets. Ordinarily, you don't have to worry about this, but it's a good idea to be aware that different charsets exist in case you run into textual data encoded in a non-default way.


11.1.4  Reading Text

Much I/O is done in the form of human-readable characters. In view of this, it is surprising that Java does not provide a standard character input class that can read character data in a manner that is reasonably symmetrical with the character output capabilities of PrintWriter. The Scanner class, introduced briefly in Subsection 2.4.6 and covered in more detail below, comes pretty close, but Scanner is not a subclass of any I/O stream class, which means that it doesn't fit neatly into the I/O stream framework. There is one basic case that is easily handled by the standard class BufferedReader, which has a method

public String readLine() throws IOException

that reads one line of text from its input source. If the end of the stream has been reached, the return value is null. When a line of text is read, the end-of-line marker is read from the input stream, but it is not part of the string that is returned. Different input streams use different characters as end-of-line markers, but the readLine method can deal with all the common cases. (Traditionally, Unix computers, including Linux and MacOS, use a line feed character, '\n', to mark an end of line; classic Macintosh used a carriage return character, '\r'; and Windows uses the two-character sequence "\r\n". In general, modern computers can deal correctly with all of these possibilities.)

BufferedReader also defines the instance method lines() which returns a value of type Stream<String> that can be used with the stream API (Section 10.6). A convenient way to process all the lines from a BufferedReader, reader, is to use the forEachOrdered() operator on the stream of lines: reader.lines().forEachOrdered(action), where action is a consumer of strings, usually given as a lambda expression.

Line-by-line processing is very common. Any Reader can be wrapped in a BufferedReader to make it easy to read full lines of text. If reader is of type Reader, then a BufferedReader wrapper can be created for reader with

BufferedReader in = new BufferedReader( reader );

This can be combined with the InputStreamReader class that was mentioned above to read lines of text from an InputStream. For example, we can apply this to System.in:

BufferedReader in;  // BufferedReader for reading from standard input.
in = new BufferedReader( new InputStreamReader( System.in ) );
try {
   String line = in.readLine();
   while ( line != null ) {  
      processOneLineOfInput( line );
      line = in.readLine();
   }
}
catch (IOException e) {
}

This code segment reads and processes lines from standard input until an end-of-stream is encountered. (An end-of-stream is possible even for interactive input. For example, on at least some computers, typing a Control-D generates an end-of-stream on the standard input stream.) The try..catch statement is necessary because the readLine method can throw an exception of type IOException, which requires mandatory exception handling; an alternative to try..catch would be to declare that the method that contains the code "throws IOException". Also, remember that BufferedReader, InputStreamReader, and IOException must be imported from the package java.io.

Note that the main purpose of BufferedReader is not simply to make it easier to read lines of text. Some I/O devices work most efficiently if data is read or written in large chunks, instead of as individual bytes or characters. A BufferedReader reads a chunk of data, and stores it in internal memory. The internal memory is known as a buffer. When you read from the BufferedReader, it will take data from the buffer if possible, and it will only go back to its input source for more data when the buffer is emptied. There is also a BufferedWriter class, and there are buffered stream classes for byte streams as well.


Previously in this book, we have used the non-standard class TextIO for input both from users and from files. The advantage of TextIO is that it makes it fairly easy to read data values of any of the primitive types. Disadvantages include the fact that TextIO can only read from one input source at a time and that it does not follow the same pattern as Java's built-in input/output classes. (If you like the style of input used by TextIO, you might take a look at my TextReader.java, which implements a similar style of input in a more object-oriented way. TextReader was used in previous versions of this textbook but is not used in this version.)


11.1.5  The Scanner Class

Since its introduction, Java has been notable for its lack of built-in support for basic input, or at least for its reliance on fairly advanced techniques for the support that it does offer. (This is my opinion, at least.) The Scanner class was introduced to make it easier to read basic data types from a character input source. It does not (again, in my opinion) solve the problem completely, but it is a big improvement. The Scanner class is in the package java.util. It was introduced in Subsection 2.4.6, but has not seen much use since then in this textbook. From now on, however, most of my examples will use Scanner instead of TextIO.

Input routines are defined as instance methods in the Scanner class, so to use the class, you need to create a Scanner object. The constructor specifies the source of the characters that the Scanner will read. The scanner acts as a wrapper for the input source. The source can be a Reader, an InputStream, a String, or a File, among other possibilities. If a String is used as the input source, the Scanner will simply read the characters in the string from beginning to end, in the same way that it would process the same sequence of characters from a stream. For example, you can use a Scanner to read from standard input by saying:

Scanner standardInputScanner = new Scanner( System.in );

and if charSource is of type Reader, you can create a Scanner for reading from charSource with:

Scanner scanner = new Scanner( charSource );

When processing input, a scanner usually works with tokens. A token is a meaningful string of characters that cannot, for the purposes at hand, be further broken down into smaller meaningful pieces. A token can, for example, be an individual word or a string of characters that represents a value of type double. In the case of a scanner, tokens must be separated by "delimiters." By default, the delimiters are whitespace characters such as spaces, tabs, and end-of-line markers. In normal processing, whitespace characters serve simply to separate tokens and are discarded by the scanner. A scanner has instance methods for reading tokens of various types. Suppose that scanner is an object of type Scanner. Then we have:

All of these methods can generate exceptions. If an attempt is made to read past the end of input, an exception of type NoSuchElementException is thrown. Methods such as scanner.getInt() will throw an exception of type InputMismatchException if the next token in the input does not represent a value of the requested type. The exceptions that can be generated do not require mandatory exception handling.

The Scanner class has very nice look-ahead capabilities. You can query a scanner to determine whether more tokens are available and whether the next token is of a given type. If scanner is of type Scanner:

Although the insistence on defining tokens only in terms of delimiters limits the usability of scanners to some extent, they are easy to use and are suitable for many applications. With so many input classes available—BufferedReader, TextIO, Scanner—you might have trouble deciding which one to use! In general, I would recommend using a Scanner unless you have some particular reason for preferring TextIO-style input. BufferedReader can be used as a lightweight alternative when all that you want to do is read entire lines of text from the input source.

(It is possible to change the delimiter that is used by a Scanner, but the syntax uses something called "regular expressions." Unfortunately, the syntax for regular expressions is rather complicated, and they are not covered in this book. However, as an example, suppose you want tokens to be words that consist entirely of letters of the English alphabet. In that case, delimiters should include all non-letter characters. If you want a Scanner, scnr, to use that kind of delimiter, you can say: scnr.useDelimiter("[^a-zA-Z]+"). After that, tokens returned by scnr.next() will consist entirely of letters. The string "[^a-zA-Z]+" is a regular expression. Regular expressions are an important tool for a working programmer. If you have a chance to learn about them, you should do so.)


11.1.6  Serialized Object I/O

The classes PrintWriter, Scanner, DataInputStream, and DataOutputStream allow you to easily input and output all of Java's primitive data types. But what happens when you want to read and write objects? Traditionally, you would have to come up with some way of encoding your object as a sequence of data values belonging to the primitive types, which can then be output as bytes or characters. This is called serializing the object. On input, you have to read the serialized data and somehow reconstitute a copy of the original object. For complex objects, this can all be a major chore. However, you can get Java to do a lot of the work for you by using the classes ObjectInputStream and ObjectOutputStream. These are subclasses of InputStream and OutputStream that can be used for reading and writing serialized objects.

ObjectInputStream and ObjectOutputStream are wrapper classes that can be wrapped around arbitrary InputStreams and OutputStreams. This makes it possible to do object input and output on any byte stream. The methods for object I/O are readObject(), in ObjectInputStream, and writeObject(Object obj), in ObjectOutputStream. Both of these methods can throw IOExceptions. Note that readObject() returns a value of type Object, which generally has to be type-cast to the actual type of the object that was read.

ObjectOutputStream also has methods writeInt(), writeDouble(), and so on, for outputting primitive type values to the stream, and ObjectInputStream has corresponding methods for reading primitive type values. These primitive type values can be interspersed with objects in the data. In the file, the primitive types will be represented in their internal binary format.

Object streams are byte streams. The objects are represented in binary, machine-readable form. This is good for efficiency, but it does suffer from the fragility that is often seen in binary data. They suffer from the additional problem that the binary format of Java objects is very specific to Java, so the data in object streams is not easily available to programs written in other programming languages. For these reasons, object streams are appropriate mostly for short-term storage of objects and for transmitting objects over a network connection from one Java program to another. For long-term storage and for communication with non-Java programs, other approaches to object serialization are usually better. (See Section 11.5 for a character-based approach. See Subsection 12.5.1 for an example that uses object streams for network communication.)

ObjectInputStream and ObjectOutputStream only work with objects that implement an interface named Serializable. Furthermore, all of the instance variables in the object must be serializable. However, there is little work involved in making an object serializable, since the Serializable interface does not declare any methods. It exists only as a marker for the compiler, to tell it that the object is meant to be writable and readable. You only need to add the words "implements Serializable" to your class definitions. Many of Java's standard classes are already declared to be serializable.

One warning about using ObjectOutputStreams: These streams are optimized to avoid writing the same object more than once. When an object is encountered for a second time, only a reference to the first occurrence is written. Unfortunately, if the object has been modified in the meantime, the new data will not be written. That is, the modified value will not be written correctly to the stream. Because of this, ObjectOutputStreams are meant mainly for use with "immutable" objects that can't be changed after they are created. (Strings are an example of this.) However, if you do need to write mutable objects to an ObjectOutputStream, and if it is possible that you will write the same object more than once, you can ensure that the full, correct version of the object will be written by calling the stream's reset() method before writing the object to the stream.


[ Next Section | Chapter Index | Main Index ]