Overview

To implement filter streams use the FilterInputStream.java and FilterOutputStream classes. Subclasses include:

  1. DataInputStream
  2. DataOutputStream
  3. BufferedInputStream
  4. BufferedOutputStream
  5. LineNumberInputStream.java
  6. PushbackInputStream.java
  7. PrintStream

You can also use the java.io.FilterReader class, which contains only one subclass: PushbackReader.

To use a filter input or output stream, attach the filter stream to another input or output stream. For example:

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

DataInputStream and DataOutputStream

DataIODemo.java reads and writes tabular data formatted in columns 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:

19.99   12      Java T-shirt
9.99    8       Java Mug

DataOutputStream, like other filtered 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.txtg:

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

Next, DataIODemo.java 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('\t');
    out.writeInt(units[i]);
    out.writeChar('\t');
    out.writeChars(descs[i]);
    out.writeChar('\n');
}
out.close();

Next, DataIODemo.java 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.txt. Then DataIODemo.java 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 of the data has been read, DataIODemo.java displays a statement summarizing the order and the total amount owed and then closes the stream.

Note the loop that DataIODemo.java 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 DataInputStreams read methods throw an EOFException instead. When the EOFException occurs, the while (true) terminates.

When you run the DataIODemo.java 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.8800000000001

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.java 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, 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.

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


CheckedOutputStream Class

CheckedOutputStream.java computes a checksum on data as it is being written to the stream. Create using the constructor:

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

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

CheckedOutputStream overrides 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.java 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 filtered stream is attached to, and then update the checksum.


CheckedInputStream

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

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

This constructor is similar to CheckedOutputStream’s.

Just as CheckedOutputStream.java needed to override FilterOutputStream’s write methods, CheckedInputStream.java must override FilterInputStream’s read methods so that each time the read method is called, the checksum is updated. As with FilterOutputStream, FilterInputStream.java defines three versions of the read method. CheckedInputStream.java 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 that this filtered stream is attached to, then if any data was actually read, update the checksum.


Checksum Interface

The Checksum interface 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. 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.

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


A Program for Testing

The last class in the example, CheckedIODemo.java, contains the main method for the program:

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 a CheckedOutputStream.java and a CheckedInputStream.java. This example requires two checksum objects because the checksum objects are updated during calls to read and write and those calls are occurring concurrently.

Next, main opens a CheckedInputStream.java on a small text file, farrago.txt, and a CheckedOutputStream.java 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.java and simply copies it to the CheckedOutputStream.java. 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.java, you should see this output:

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