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 I/O abstractions are called streams. This section is an introduction to streams, but it is not meant to cover them in full detail. See the official Java documentation for more information. Also note that a few important general facts about streams are deferred until the next two sections of this chapter, where they are actually used.
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 data. Machine-formatted data is represented in 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 many predefined classes for representing streams of each type.
Every 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 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 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 Western alphabets, character data is generally stored in files in ASCII code, which uses only 8 bits per characters. The Reader and Writer classes take care of this translation, and can also handle non-western alphabets in countries that use them.)
It's usually easy to decide whether to use byte streams or character streams. If you want the data to be human-readable, use character streams. Otherwise, use byte streams. (I should note that Java 1.0 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, as of Java 1.1, you should use Readers and Writers rather than InputStreams and OutputStreams when working with character data.)
All the stream classes 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. Streams are not used in Java's graphical user interface, which has its own form of I/O. But they are necessary for working with files (using the classes FileInputStream, FileOutputStream, FileReader, and FileWriter) and for doing communication over a network. They can be also used for communication between two concurrently running threads.
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 fairly primitive I/O operations. For example, the InputStream class declares the instance methodpublic int read() throws IOException
for reading one byte of data (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 IOException is thrown. Since IOException is an exception class that requires mandatory exception-handling, 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.
The InputStream class also defines methods for reading several bytes of data in one step into an array of 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'll never 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 methodpublic void write(int b) throws IOException
but again, in practice, you will almost always use higher-level output operations defined in some subclass of OutputStream.
The Reader and Writer classes provide very similar low-level read and write operations. But in these character-oriented classes, the I/O operations read and write char values rather than bytes. In practice, you will use sub-classes of Reader and Writer, as discussed below.
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. Objects that can be used as wrappers in this way belong to subclasses of FilterInputStream and FilterOutputStream for byte streams, and to subclasses of FilterReader and FilterWriter for character streams. By writing new subclasses of these classes, you can make your own I/O filters to provide any style of I/O that you want.
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 sayPrintWriter printableCharSink = new PrintWriter(charSink);
When you output data to printableCharSink, using PrintWriter's advanced data output methods, 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 stream.
For the record, the output methods of the PrintWriter class include:public void print(String s) // methods for outputting public void print(char c) // standard data types public void print(int i) // to the stream, in public void print(long l) // human-readable form. public void print(float f) public void print(double d) public void print(boolean b) public void println() // output a carriage return to the stream public void println(String s) // these methods are identical public void println(char c) // to the previous set, public void println(int i) // except that the output public void println(long l) // value is followed by public void println(float f) // a carriage return public void println(double d public void println(boolean b)
Note that none of these methods will ever throw an IOException. Instead, the PrintWriter class includes the methodpublic 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 every time you use a PrintWriter method.
When you use PrintWriter methods to output data to a stream, the data is converted into the sequence of characters that represent the data in human-readable form. Suppose you want to output the data in byte-oriented, computer-readable 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. DataOuputStream bears the same reationship 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 sayDataOutputStream dataSink = new DataOutputStream(byteSink);
to wrap byteSink in a DataOuputStream, dataSink.
For inputing machine-readable data, like 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, and vice versa. 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.
Still, the fact remains that 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. Fortunately, Java's object-oriented nature makes it possible to write such a class and then use it in exactly the same way as if it were a standard part of the language.
I have written a class called TextReader that allows convenient input of data written out in human-readable character format. The source code for this class is available if you want to read it. The TextReader class is a subclass of FilterReader, which means that you can use a TextReader as a wrapper for another input stream. The constructorpublic TextReader(Reader dataSource)
creates an object that can be used to read data from the given Reader, dataSource, using the convenient input methods of the TextReader class. These methods include:public char peek() // Look at the next character in the stream, // without removing it from the stream. If all // the characters in the stream have already // been read, then the character '\0' is // returned. If the next character in the // stream is a carriage return, then a '\n' // is returned. public void skipWhiteSpace() // Read and discard whitespace characters // (space, carriage return, tab), until // a non-whitespace character is seen. public boolean eoln() // Discards any spaces and tabs in the stream, // then tests whether the next char is // the end of the current line (or the // end of the data in the stream). public boolean eof() // Discards any whitespace characters, then // returns true if all the characters // in the stream have been read. public char readChar() // These routines read values of the public byte readByte() // specified types. In each case, public short readShort() // the computer will skip any whitespace public int readInt() // characters before trying to read a public long readLong() // value of the specified type. public float readFloat() // An error occurs if a value of the public double readDouble() // correct type is not found. For public String readWord() // the readWord() routine, a word is public boolean readBoolean() // considered to be any string of // non-blank characters. For readBoolean(), // the input can be any of the strings // "true", "false", "t", "f", "yes", // "no", "y", "n", "1", or "0", ignoring case. public String getln(); // Reads characters up to the end of the // current line of input. Then reads // and discards the carriage return character. // Note that this routine does NOT skip // over leading whitespace characters, and // that the value returned might be the // empty string. public char getlnChar(); // These routines are provided as a public byte getlnByte(); // convenience. They are equivalent public short getlnShort(); // to the above routines, except that public int getlnInt(); // after successfully reading a value of public long getlnLong(); // the specified type, the computer public float getlnFloat(); // reads and discards any remaining public double getlnDouble(); // characters on the same line of input. public String getlnString(); public boolean getlnBoolean();
For convenience, I also make it possible to wrap an InputStream in a TextReader object. For example, since System.in is of type InputStream, you could say:TextReader in = new TextReader(System.in);
You could then use in.getInt() to read an integer from standard input, in.getBoolean() to read a boolean value, and so on. Similarly, you can read character data from file and network connections by wrapping the input streams for those objects in TextReader. In an exactly symmetrical way, you can wrap an OutputStream in a PrintWriter if you want to write character data to the stream.
There remains the question of what happens when an error occurs while one of the output routines in the TextReader class is being executed. Whoever designed the PrintWriter class decided not to throw exceptions when errors occur. When I designed TextReader, I decided to give you a choice. By default, a routine that encounters an error will throw an exception belonging to the class TextReader.Error. This is a static nested class declared inside the TextReader class. It is a subclass of the RuntimeException class. Now, recall that you don't have to use try and catch to deal with RuntimeExceptions; however, if one occurs and is not caught, it will crash your program. If you prefer not to deal with exceptions at all, you can turn off this behavior by calling the TextReader instance methodpublic void checkIO(boolean throwExceptions)
with its parameter set to false. In that case, when an error occurs during input, no exception will be thrown. Instead, the value of an internal error flag will be set, and the program will continue. If you use this option, it is your responsibility to check for errors after each input operation. You can do this with the instance methodpublic boolean checkError()
This method returns true if the most recent input operation on the TextReader produced an error, and it returns false if that operation completed successfully. It is probably easier to write robust programs by catching and handling exceptions than by continually checking for possible errors. With both options available, you can experiment with both styles of exception-handling and see which one you prefer.
The classes PrintWriter, TextReader, 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 all the work for you by using the classes ObjectInputStream and ObjectOutputStream. These are subclasses of InputStream and Outputstream that can be used for writing and reading 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 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 a more useful type.
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, including all the component classes in the AWT.
[ Next Section | Previous Section | Chapter Index | Main Index ]