Trail: Essential Java Classes
|
Lesson: Reading and Writing (but no 'rithmetic)
|
|
Writing Your Own Filtered Streams
The following is a list of steps to take when writing your own filtered
input and output streams:
-
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.
-
Override the
read and write methods.
-
Override any other methods that you might need.
-
Make sure the input and output streams work together.
This section shows you how to implement your own filtered streams
through an example that implements a matched pair of filtered input and
output streams. Many thanks to David Connelly from the Java team
for providing us with this example.
Both the input and the output stream 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:
-
The filtered input and output stream subclasses--
CheckedOutputStream and CheckedInputStream.
-
The
Checksum interface and the Adler32 class compute a checksum for the streams.
-
The
CheckedIOTest class defines the main method for the program.
The CheckedOutputStream Class
The CheckedOutputStream
class is a subclass of FilterOutputStream that computes a checksum on
data as it's being written to the stream. When creating a CheckedOutputStream,
you must use its only constructor:
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:
-
write(int i)
-
write(byte[] b)
-
write(byte[] b, int offset, int length)
CheckedOutputStream overrides all three of these methods:
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, then update the checksum.
The CheckedInputStream Class
The CheckedInputStream
class is very similar to the CheckedOutputStream class.
CheckedInputStream is a subclass of FilterInputStream that computes a checksum on
data as it's being read from the stream. When creating a CheckedInputStream,
you must use its only constructor:
public CheckedInputStream(InputStream in, Checksum cksum) {
super(in);
this.cksum = cksum;
}
This constructor is similar to the constructor for CheckedOutputStream.
Just as CheckedOutputStream needed to override FilterOutputStream's
write methods, CheckedInputStream must override FilterInputStream's
read methods so that each time the read method is called,
the checksum is updated. As with FilterOutputStream, FilterInputStream defines
three versions of the read method and CheckedInputStream
override all of them:
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, len);
}
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.
The Checksum Interface and the Adler32 Class
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 go. 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 Adler32
checksum, which is almost as reliable as a CRC-32 checksum but can be
computed much faster.
A Program for Testing
The last class in the example, CheckedIOTest,
contains the main method for the program.
import java.io.*;
public class CheckedIOTest {
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("CheckedIOTest: " + e);
System.exit(-1);
} catch (IOException e) {
System.err.println("CheckedIOTest: " + 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 and a
CheckedInputStream. The 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 on a
small text file, 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 consequently 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 CheckedIOTest, you should see this output:
Input stream check sum: 736868089
Output stream check sum: 736868089
Filtering Random Access Files
The filtered streams in java.io all inherit from InputStream or OutputStream,
which implement sequential access files. So if you subclass FilterInputStream
or FilterOutputStream your filtered streams will also be sequential access files.
Writing Filters for Random Access Files
later in this lesson shows you how to re-write this example so that it works on a
RandomAccessFile as well as on a DataInputStream or a DataOutputStream.
|