Using the Streams

Table 47 lists java.io's streams and describes what they do. Note that many times, java.io contains character streams and byte streams that perform the same type of I/O but for different data types.

Table 47. I/O Streams

Type of I/O

Streams

Description

Memory

CharArrayReader CharArrayWriter ByteArrayInputStream ByteArrayOutputStream

Use these streams to read from and write to memory. You create these streams on an existing array and then use the read and write methods to read from or write to the array.

StringReader StringWriter StringBufferInputStream

Use StringReader to read characters from a String in memory. Use StringWriter to write to a String. StringWriter collects the characters written to it in a StringBuffer, which can then be converted to a String. StringBufferInputStream is similar to StringReader, except that it reads bytes from a StringBuffer.

Pipe

PipedReader PipedWriter PipedInputStream PipedOutputStream

Implement the input and output components of a pipe. Pipes are used to channel the output from one thread into the input of another. See PipedReader and PipedWriter in action in the section How to Use Pipe Streams (page 322).

File

FileReader FileWriter FileInputStream FileOutputStream

Collectively called file streams, these streams are used to read from or write to a file on the native file system. The section How to Use File Streams (page 320) has an example that uses FileReader and FileWriter to copy the contents of one file into another.

Concatenation

N/A SequenceInputStream

Concatenates multiple input streams into one input stream. The section How to Concatenate Files (page 325) has a short example of this class.

Object Serialization

N/A ObjectInputStream ObjectOutputStream

Used to serialize objects. See the section Object Serialization (page 334).

Data Conversion

N/A DataInputStream DataOutputStream

Read or write primitive data types in a machine-independent format. The section How to Use DataInputStream and DataOutputStream (page 328) shows an example of using these two streams.

Counting

LineNumberReader LineNumberInputStream

Keeps track of line numbers while reading.

Peeking Ahead

PushbackReader PushbackInputStream

These input streams each have a pushback buffer. When reading data from a stream, it is sometimes useful to peek at the next few bytes or characters in the stream to decide what to do next.

Printing

PrintWriter PrintStream

Contain convenient printing methods. These are the easiest streams to write to, so you will often see other writable streams wrapped in one of these.

Buffering

BufferedReader BufferedWriter BufferedInputStream BufferedOutputStream

Buffer data while reading or writing, thereby reducing the number of accesses required on the original data source. Buffered streams are typically more efficient than similar nonbuffered streams and are often used with other streams.

Filtering

FilterReader FilterWriter FilterInputStream FilterOutputStream

These abstract classes define the interface for filter streams, which filter data as it's being read or written. The section Working with Filter Streams (page 327) shows you how to use filter streams and how to implement your own.

Converting between Bytes and Characters

InputStreamReader OutputStreamWriter

A reader and writer pair that forms the bridge between byte streams and character streams.

An InputStreamReader reads bytes from an InputStream and converts them to characters, using the default character encoding or a character encoding specified by name.

An OutputStreamWriter converts characters to bytes, using the default character encoding or a character encoding specified by name and then writes those bytes to an OutputStream.

You can get the name of the default character encoding by calling System.getProperty("file.encoding").

The next several sections provide examples on how to use several of these streams.

How to Use File Streams

File streams are perhaps the easiest streams to understand. The file streamsFileReader, [1] FileWriter, [2] FileInputStream, [3] and FileOutputStream [4]each read or write from a file on the native file system. You can create a file stream from a file name in the form of a string, a File [5] object, or a FileDescriptor [6] object.

[1] http://java.sun.com/j2se/1.3/docs/api/java/io/FileReader.html

[2] http://java.sun.com/j2se/1.3/docs/api/java/io/FileWriter.html

[3] http://java.sun.com/j2se/1.3/docs/api/java/io/FileInputStream.html

[4] http://java.sun.com/j2se/1.3/docs/api/java/io/FileOutputStream.html

[5] http://java.sun.com/j2se/1.3/docs/api/java/io/File.html

[6] http://java.sun.com/j2se/1.3/docs/api/java/io/FileDescriptor.html

The following program, Copy, [1] uses FileReader and FileWriter to copy the contents of a file named farrago.txt into a file called outagain.txt:

[1] Copy.java and the text file farrago.txt are included on the CD and are available online. See Code Samples (page 348).

import java.io.*; public class Copy { public static void main(String[] args) throws IOException { File inputFile = new File("farrago.txt"); File outputFile = new File("outagain.txt"); FileReader in = new FileReader(inputFile); FileWriter out = new FileWriter(outputFile); int c; while ((c = in.read()) != -1) { out.write(c); } in.close(); out.close(); } }

This program is very simple. It opens a file reader on farrago.txt and opens a file writer on outagain.txt. The program reads characters from the reader as long as there's more input in the input file and writes those characters to the writer. When the input runs out, the program closes both the reader and the writer.

Here is the code that the Copy program uses to create a file reader:

File inputFile = new File("farrago.txt"); FileReader in = new FileReader(inputFile);

This code creates a File object that represents the named file on the native file system. File is a utility class provided by java.io. The Copy program uses this object only to construct a file reader on a file. However, the program could use inputFile to get information, such as its full path name, about the file.

After you've run the program, you should find an exact copy of farrago.txt in a file named outagain.txt in the same directory. Here is the content of the file:

So she went into the garden to cut a cabbage-leaf, to make an apple-pie; and at the same time a great she-bear, coming up the street, pops its head into the shop. 'What! no soap?' So he died, and she very imprudently married the barber; and there were present the Picninnies, and the Joblillies, and the Garyalies, and the grand Panjandrum himself, with the little round button at top, and they all fell to playing the game of catch as catch can, till the gun powder ran out at the heels of their boots. - Samuel Foote 17201777

Remember that FileReader and FileWriter read and write 16-bit characters. However, most native file systems are based on 8-bit bytes. These streams encode the characters as they read or write according to the default character-encoding scheme. You can find out the default character encoding by using System.getProperty("file.encoding"). To specify an encoding other than the default, you should construct an OutputStreamWriter on a FileOutputStream and specify the encoding. For information about encoding characters, see the "Writing Global Programs" section of the Internationalization chapter; this chapter is included in The Java Tutorial Continued book and online. [1]

[1] http://java.sun.com/docs/books/tutorial/i18n/index.html

Another version of this program, CopyBytes.java, [2] uses FileInputStream and FileOutputStream instead of FileReader and FileWriter. This program is available on this book's CD and online.

[2] CopyBytes.java is included on the CD and is available online. See Code Samples (page 348).

How to Use Pipe Streams

Pipes are used to channel the output from one thread into the input of another. PipedReader [3] and PipedWriter [4] (and their input and output stream counterparts PipedInputStream [5] and PipedOutputStream [6]) implement the input and output components of a pipe. Why are pipes useful?

[3] http://java.sun.com/j2se/1.3/docs/api/java/io/PipedReader.html

[4] http://java.sun.com/j2se/1.3/docs/api/java/io/PipedWriter.html

[5] http://java.sun.com/j2se/1.3/docs/api/java/io/PipedInputStream.html

[6] http://java.sun.com/j2se/1.3/docs/api/java/io/PipedOutputStream.html

Consider a class that implements various string manipulation utilities, such as sorting and reversing text. It would be nice if the output of one of these methods could be used as the input for another so that you could string a series of method calls together to perform a higher-order function. For example, you could reverse each word in a list, sort the words, and then reverse each word again to create a list of rhyming words.

Without pipe streams, a program would have to store the results somewhere (such as in a file or in memory) between each step, as shown in Figure 91.

Figure 91. Without a pipe, a program must store intermediate results.

With pipe streams, the output from one method is the input for the next method, as shown in Figure 92.

Figure 92. With a pipe, a program can pipe information directly from one thread to another.

Let's look at a program, called RhymingWords, [1] that implements what's represented in Figure 92. This program uses PipedReader and PipedWriter to connect the input and output of its reverse and sort methods to create a list of rhyming words. Several classes make up this program.

[1] RhymingWords.java is included on the CD and is available online. The other files you need for this example are words.txt, ReverseThread.java, and SortThread.java. See Code Samples (page 348).

First, let's look at the calling sequence of the reverse and sort methods from the main method:

FileReader words = new FileReader("words.txt"); Reader rhymingWords = reverse(sort(reverse(words)));

The innermost call to reverse takes a FileReader, which is opened on the file words.txt, which contains a list of words. [1] The return value of reverse is passed to sort, whose return value is then passed to another call to reverse.

[1] The file words.txt is included on the CD and is available online. See Code Samples (page 348).

Let's look at the reverse method; the sort method is similar, and you will understand it once you understand reverse.

public static Reader reverse(Reader src) throws IOException { BufferedReader in = new BufferedReader(source); PipedWriter pipeOut = new PipedWriter(); PipedReader pipeIn = new PipedReader(pipeOut); PrintWriter out = new PrintWriter(pipeOut); new ReverseThread(out, in).start(); return pipeIn; }

The statements in boldface create both ends of a pipea PipedWriter and a PipedReaderand connect them by constructing the PipedReader "on" the PipedWriter. Whatever is written to the PipedWriter can be read from the PipedReader. The connection forms a pipe, as illustrated in Figure 93.

Figure 93. Using PipedWriter and PipedReader to form a pipe.

The reverse method starts a ReverseThread that writes its output to the PipedWriter and returns the PipedReader to the caller. The caller then arranges for a sorting thread to read from it. The sort method is exactly the same, except that it creates and starts a SortThread. [2]

[2] WriteReverseThread.java and SortThread.java are included on the CD and are available online. See Code Samples (page 348).

How to Wrap a Stream

The reverse method contains some other interesting code; in particular, these two statements:

BufferedReader in = new BufferedReader(source); ... PrintWriter out = new PrintWriter(pipeOut);

This code opens a BufferedReader on source, which is another reader of a different type. This essentially "wraps" source in a BufferedReader. The program reads from the BufferedReader, which in turn reads from source. The program does this so that it can use BufferedReader's convenient readLine method. Similarly, the PipedWriter is wrapped in a PrintWriter so that the program can use PrintWriter's convenient println method. You will often see streams wrapped in this way so as to combine the various features of the many streams.

How to Concatenate Files

SequenceInputStream [1] creates a single input stream from multiple input sources. This example program, Concatenate, [2] uses SequenceInputStream to implement a concatenation utility that sequentially concatenates files together in the order in which they are listed on the command line.

[1] http://java.sun.com/j2se/1.3/docs/api/java/io/SequenceInputStream.html

[2] Concatenate.java is included on the CD and is available online. See Code Samples (page 348).

The following is the controlling class of the Concatenate utility:

import java.io.*; public class Concatenate { public static void main(String[] args) throws IOException { ListOfFiles mylist = new ListOfFiles(args); SequenceInputStream s = new SequenceInputStream(mylist); int c; while ((c = s.read()) != -1) { System.out.write(c); } s.close(); } }

First, the Concatenate utility creates a ListOfFiles object named mylist, which is initialized from the command line arguments entered by the user. The mylist object is an enumeration that SequenceInputStream uses to obtain a new InputStream whenever it needs one.

import java.util.*; import java.io.*; public class ListOfFiles implements Enumeration { private String[] listOfFiles; private int current = 0; public ListOfFiles(String[] listOfFiles) { this.listOfFiles = listOfFiles; } public boolean hasMoreElements() { if (current < listOfFiles.length) { return true; } else { return false; } } public Object nextElement() { InputStream in = null; if (!hasMoreElements()) { throw new NoSuchElementException("No more files."); } else { String nextElement = listOfFiles[current]; current++; try { in = new FileInputStream(nextElement); } catch (FileNotFoundException e) { System.out.println("ListOfFiles: Can't open" + nextElement); } } return in; } }

ListOfFiles implements the Enumeration [1] interface. You'll see how this comes into play as you walk through the rest of the program.

[1] http://java.sun.com/j2se/1.3/docs/api/java/util/Enumeration.html

After creating the SequenceInputStream, the main method reads from that stream one byte at a time. When it needs an InputStream from a new source, such as for the first byte read or when it runs off the end of the current input stream, the SequenceInputStream calls nextElement on the Enumeration object to get the next InputStream. ListOfFiles creates FileInputStream objects lazily. This means that whenever SequenceInputStream calls nextElement, ListOfFiles opens a FileInputStream on the next file name in the list and returns the stream. When the ListOfFiles object runs out of files to read (it has no more elements), nextElement returns null, and the call to SequenceInputStream's read method returns 1 to indicate the end of input.

Try This

Run Concatenate on the farrago.txt and words.txt files; both are text files used as input to other examples in this chapter.

 

Working with Filter Streams

The java.io package provides a set of abstract classes that define and partially implement filter streams. A filter stream filters data as it's being read from or written to the stream. The filter streams are FileReader, [1] FileWriter, [2] FileInputStream, [3] and FileOutputStream. [4]

[1] http://java.sun.com/j2se/1.3/docs/api/java/io/FileReader.html

[2] http://java.sun.com/j2se/1.3/docs/api/java/io/FileWriter.html

[3] http://java.sun.com/j2se/1.3/docs/api/java/io/FileInputStream.html

[4] http://java.sun.com/j2se/1.3/docs/api/java/io/FileOutputStream.html

A filter stream is constructed on another stream (the underlying stream). The read method in a readable filter stream reads input from the underlying stream, filters it, and passes on the filtered data to the caller. The write method in a writable filter stream filters the data and then writes it to the underlying stream. The filtering done by the streams depends on the stream. Some streams buffer the data, some count data as it goes by, and others convert data to another form.

Most filter streams provided by the java.io package are subclasses of FilterInputStream and FilterOutputStream and are listed here:

The java.io package contains only one subclass of FilterReader: PushbackReader. So this section focuses on filter byte streams.

This section shows you how to use filter streams by presenting an example that uses a DataInputStream and a DataOutputStream. This section also covers how to subclass FilterInputStream and FilterOutputStream to create your own filter streams.

Using Filter Streams

To use a filter input or output stream, attach the filter stream to another input or output stream when you create it. For example, you can attach a DataInputStream to the standard input stream, as in the following code:

DataInputStream in = new DataInputStream(System.in); String input; while ((input = in.readLine()) != null) { ... //do something interesting here }

You might do this so that you can use the more convenient read methods, such as readLine, implemented by DataInputStream.

How to Use DataInputStream and DataOutputStream

This section features an example, DataIODemo, [1] that reads and writes tabular data (invoices for merchandise). The tabular data is formatted in columns and separated by tabs. The columns contain the sales price, the number of units ordered, and a description of the item. Conceptually, the data looks like this, although it is read and written in binary form and is non-ASCII:

[1] DataIODemo.java is included on the CD and is available online. See Code Samples (page 348).

19.99 12 Java T-shirt 9.99 8 Java Mug

DataOutputStream, like other filter output streams, must be attached to another OutputStream. In this case, it's attached to a FileOutputStream that is set up to write to a file named invoice1.txt:

DataOutputStream out = new DataOutputStream(new FileOutputStream("invoice1.txt"));

Next, DataIODemo uses DataOutputStream's specialized write methods to write the invoice data contained within arrays in the program according to the type of data being written:

for (int i = 0; i < prices.length; i++) { out.writeDouble(prices[i]); out.writeChar(' '); out.writeInt(units[i]); out.writeChar(' '); out.writeChars(descs[i]); out.writeChar(' '); } out.close();

Next, DataIODemo opens a DataInputStream on the file just written:

DataInputStream in = new DataInputStream(new FileInputStream("invoice1.txt"));

DataInputStream also must be attached to another InputStream, in this case, a FileInputStream set up to read the file just written, invoice1. Then DataIODemo just reads the data back in, using DataInputStream's specialized read methods:

try { while (true) { price = in.readDouble(); in.readChar(); //throws out the tab unit = in.readInt(); in.readChar(); //throws out the tab char chr; desc = new StringBuffer(20); char lineSep = System.getProperty("line.separator").charAt(0); while ((chr = in.readChar() != lineSep) { desc.append(chr); } System.out.println("You've ordered " + unit + " units of " + desc + " at $" + price); total = total + unit * price; } } catch (EOFException e) { } System.out.println("For a TOTAL of: $" + total); in.close();

When all the data has been read, DataIODemo displays a statement summarizing the order and the total amount owed and then closes the stream.

Note the loop that DataIODemo uses to read the data from the DataInputStream. Normally, when data is read, you see loops like this:

while ((input = in.read()) != null) { ... }

The read method returns a value, null, which indicates that the end of the file has been reached. Many of the DataInputStream read methods can't do this, because any value that could be returned to indicate the end of file may also be a legitimate value read from the stream. For example, suppose that you want to use 1 to indicate end of file. Well, you can't, because 1 is a legitimate value that can be read from the input stream, using readDouble, readInt, or one of the other methods that reads numbers. So DataInputStream's read methods throw an EOFException instead. When the EOFException occurs, the while (true) terminates.

When you run the DataIODemo program, you should see the following output:

You've ordered 12 units of Java T-shirt at $19.99 You've ordered 8 units of Java Mug at $9.99 You've ordered 13 units of Duke Juggling Dolls at $15.99 You've ordered 29 units of Java Pin at $3.99 You've ordered 50 units of Java Key Chain at $4.99 For a TOTAL of: $892.88

How to Write Your Own Filter Streams

Following are the steps to take when you are writing your own filtered input and output streams:

  1. Create a subclass of FilterInputStream and FilterOutputStream. Input and output streams often come in pairs, so it's likely that you will need to create both input and output versions of your filter stream.
  2. Override the read and write methods, and any others, if you need to.
  3. Provide any new methods.
  4. Make sure that the input and output streams work together.

This section shows you how to implement your own filter streams, presenting an example that implements a matched pair of filter input and output streams.

Both the input and the output streams use a checksum class to compute a checksum on the data written to or read from the stream. The checksum is used to determine whether the data read by the input stream matches that written by the output stream.

Four classes and one interface make up this example program:

Except for CheckedIODemo, the classes in this example are based on classes, which are now members of the java.util.zip package, written by David Connelly.

The CheckedOutputStream Class

The CheckedOutputStream [1] class, a subclass of FilterOutputStream, computes a checksum on data as it is being written to the stream. When creating a CheckedOutputStream, you must use its only constructor:

[1] CheckedOutputStream.java is included on the CD and is available online. See Code Samples (page 348).

public CheckedOutputStream(OutputStream out, Checksum cksum) { super(out); this.cksum = cksum; }

This constructor takes an OutputStream argument and a Checksum argument. The OutputStream argument is the output stream that this CheckedOutputStream should filter. The Checksum argument is an object that can compute a checksum. CheckedOutputStream initializes itself by calling its superclass constructor and initializing a private variable, cksum, with the Checksum object. The CheckedOutputStream uses cksum to update the checksum each time data is written to the stream.

CheckedOutputStream needs to override FilterOutputStream's write methods so that each time the write method is called, the checksum is updated. FilterOutputStream defines three versions of the write method. CheckedOutputStream overrides all three of these methods with the following code:

public void write(int b) throws IOException { out.write(b); cksum.update(b); } public void write(byte[] b) throws IOException { out.write(b, 0, b.length); cksum.update(b, 0, b.length); } public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); cksum.update(b, off, len); }

The implementations of these three write methods are straightforward: Write the data to the output stream that this filter stream is attached to, and then update the checksum.

The CheckedInputStream Class

The class CheckedInputStream [1] is similar to the CheckedOutputStream class. A subclass of FilterInputStream, it computes a checksum on data as it is read from the stream. When creating a CheckedInputStream, you must use its only constructor:

[1] CheckedInputStream.java is included on the CD and is available online. See Code Samples (page 348).

public CheckedInputStream(InputStream in, Checksum cksum) { super(in); this.cksum = cksum; }

This constructor is similar to CheckedOutputStream's.

Just as CheckedOutputStream needed to override FilterOutputStream's write methods, CheckedInputStream must override FilterInputStream's read methods. This is to ensure that each time the read method is called, the checksum is updated. As with FilterOutputStream, FilterInputStream defines three versions of the read method. CheckedInputStream overrides all of them by using the following code:

public int read() throws IOException { int b = in.read(); if (b != -1) { cksum.update(b); } return b; } public int read(byte[] b) throws IOException { int len; len = in.read(b, 0, b.length); if (len != -1) { cksum.update(b, 0, b.length); } return len; } public int read(byte[] b, int off, int len) throws IOException { len = in.read(b, off, len); if (len != -1) { cksum.update(b, off, len); } return len; }

The implementations of these three read methods are straightforward: Read the data from the input stream to which this filter stream is attached. If any data was read, update the checksum.

The Checksum Interface and the Adler32 Class

The interface Checksum.java [1] defines four methods for checksum objects to implement. These methods reset, update, and return the checksum value. You could write a Checksum class that computes a specific type of checksum, such as the CRC-32 checksum. [2] Note that inherent in the checksum is the notion of state. The checksum object doesn't just compute a checksum in one pass. Rather, the checksum is updated each time information is read from or written to the stream for which this object computes a checksum. If you want to reuse a checksum object, you must reset it.

[1] Checksum.java is included on the CD and is available online. See Code Samples (page 348).

[2] You can find more CRC-32 information here: http://www.freesoft.org/CIE/RFC/1510/78.htm

For this example, we implemented the checksum Adler32, [3] which is almost as reliable as a CRC-32 checksum but can be computed more quickly.

[3] Adler32.java is included on the CD and is available online. See Code Samples (page 348).

A Program for Testing

The last class in the example, CheckedIODemo, [4] contains the main method for the program:

[4] CheckedIODemo.java is included on the CD and is available online. See Code Samples (page 348).

import java.io.*; public class CheckedIODemo { public static void main(String[] args) throws IOException { Adler32 inChecker = new Adler32(); Adler32 outChecker = new Adler32(); CheckedInputStream in = null; CheckedOutputStream out = null; try { in = new CheckedInputStream( new FileInputStream("farrago.txt"), inChecker); out = new CheckedOutputStream( new FileOutputStream("outagain.txt"), outChecker); } catch (FileNotFoundException e) { System.err.println("CheckedIODemo: " + e); System.exit(-1); } catch (IOException e) { System.err.println("CheckedIODemo: " + e); System.exit(-1); } int c; while ((c = in.read()) != -1) { out.write(c); } System.out.println("Input stream check sum: " + inChecker.getValue()); System.out.println("Output stream check sum: " + outChecker.getValue()); in.close(); out.close(); } }

The main method creates two Adler32 checksum objects, one each for CheckedOutputStream and CheckedInputStream. This example requires two checksum objects because the checksum objects are updated during calls to read and write, and those calls occur concurrently.

Next, main opens a CheckedInputStream on a small text file named farrago.txt and a CheckedOutputStream on an output file named outagain.txt, which doesn't exist until you run the program for the first time. The main method reads the text from the CheckedInputStream and simply copies it to the CheckedOutputStream. The read and write methods use the Adler32 checksum objects to compute a checksum during reading and writing. After the input file has been completely read and the output file has been completely written, the program prints out the checksum for both the input and output streams (which should match) and then closes them both.

When you run CheckedIODemo, you should see this (or similar) output:

Input stream check sum: 736868089 Output stream check sum: 736868089

Категории