Session 26

Streams, The Sequel


810:062
Computer Science II
Object-Oriented Programming


Opening Exercise

Write a main() function that echoes the standard input to the standard output.

For example:

    mac os x > java Cat
    Michigan State had a great run in March Madness!
    Michigan State had a great run in March Madness!
    When does this semester end???
    When does this semester end???
    Aaaaaaaggggggghhhhhh!!!!!!!
    Aaaaaaaggggggghhhhhh!!!!!!!

    mac os x > java Cat
    mac os x >

Unix's cat command behaves like the Cat class shown above when it is given zero arguments:

    mac os x > cat
    Michigan State had a great run in March Madness!
    Michigan State had a great run in March Madness!
    When does this semester end???
    When does this semester end???
    Aaaaaaaggggggghhhhhh!!!!!!!
    Aaaaaaaggggggghhhhhh!!!!!!!

    mac os x > cat
    mac os x >


An Opening Solution

Here is a simple solution. Remember thatSystem.in is an InputStream, and InputStreams respond to the read() message with the next character in the stream, as an int. If the stream is empty, it returns -1.

We can also implement the body of this method using a loop and a half, as we have done in the past with BufferedReader loops. I prefer this solution, because it eliminates the duplicated read() message. It also looks the way that I think about the solution: "read a character; if the stream is empty, break; print the character".


Last Time: Polymorphism in the java.io Package

Java I/O is defined in terms of a number of polymorphic classes. Last session, we discussed the physical input streams, which read from an actual source of characters. We focused on two particular classes, FileInputStream and StringBufferInputStream that are most useful for everyday tasks.

Session 25 closed with a discussion of SequenceInputStream, a wonderful example of polymorphism. A SequenceInputStream holds a collection of InputStreams, serving characters to clients as if they were a single stream.

SequenceInputStream is a composite stream

But it is itself an InputStream, which mean that we can substitute a SequenceInputStream in place of any other InputStream. The SequenceInputStream class is an example of the Composite Pattern, which we first learned about in the context of nested panels in the java.awt package.


A Real Example Using a StringBufferInputStream

Okay, but when would we use a StringBufferInputStream -- really?

You may recall that we encountered a problem writing tests to verify our code for Homework 4. The problem was that our new behaviors involved access to files, which are outside of our test case. Having test resources lie outside of our test class causes is not ideal for several reasons:

But now we have an alternative approach. For discussion's sake, let's consider the loadFromFile() method. How can we test this method without actually accessing a file? By writing our method in terms of an InputStream. Thanks to substitutability, a FileInputStreams look just like every other stream!

We can do this in two steps:

  1. Write a loadFromFile() method that takes an InputStream as an argument.

  2. Once our secondary loadFromFile() method is complete and passes all of its tests, write the loadFromFile() method that takes a filename String argument. It can simply call the first loadFromFile() method, passing a FileInputStream initialized with the filename.

Now, our test case for loadFromFile() stands on its own. It requires no outside resources, and so can be read, understood, and run all on its own.

We can, course, do the same thing for loadFromFileSorted(). The sample solution available for download does that. Does it surprise you that we can also use this idea to test our saveToFile() method? We can -- using a StringWriter, or a StringBufferOutputStream of our own creation!

In this situation, polymorphism helps solve a problem that otherwise we would simply have to be tolerate. The solution doesn't take too much more work than the code requires anyway, and the results are valuable. (I also think it's quite elegant.)


Virtual Input Streams

Last time, we studied input streams in Java, in particular the so-called physical input streams. A physical input stream has a specific physical source of the characters it provides: a file on disk, a string, and so on. We also saw a composite stream that wraps a collection of streams and returns all the characters from all of them.

Java provides one more class of InputStreams. A virtual input stream is a stream that uses another InputStream as its source of data. Whenever the user of a virtual input stream sends it a read() message, the virtual stream delegates the job of producing a character to its helper stream.

What have I just described?

In Java, virtual input streams
are implemented as decorators!

I asked you to read about two of Java's virtual input streams for today, BufferedInputStream and FilterInputStream. From that reading, we now understand this piece of magic:

        BufferedReader inputFile =
            new BufferedReader( new FileReader( args[0] ) );

after all: a BufferedInputStream is a decorator!

Here is what the class hierarchy looks like:

the full input stream hierarchy

If you have forgotten what a decorator is, you will want to review Session 23 and Session 24.


The FilterInputStream Class

FilterInputStream is a generic decorator of InputStreams.

generic decorator

A FilterInputStream holds an InputStream instance variable of named in to which it delegates all messages that it receives.

(The instance variable is protected -- ack! -- but that's just another small example of bad design in the base packagaes.)

Programmers can then create new decorators by writing a subclass of FilterInputStream and specializing the appropriate methods -- usually just by overriding read().

BufferedInputStream works just like the BufferedReader class we've been using for file processing. It reads its characters from another InputStream, but reads them in chunks bigger than a single character (for efficiency's sake) and lets clients access whole lines of characters at a time.


Writing an InputStream Class

Back in Session 10, we found a need for text in all lower case when analyzing words in documents. There, we used String's ability to return a lowercase version of itself. But if we are using an InputStream, we can do the same job ourselves, and do it more efficiently.

Let's create an input stream that reads from any other input stream and translates each character it reads into lowercase before returning it. We will call this class LowercaseInputStream. Because it reads its characters from another stream, we will make LowercaseInputStream a virtual stream -- a subclass of FilterInputStream.

These utility functions provided by Java turn out to be quite handy:

    char    Character.toLowerCase( aChar )
    boolean Character.isUpperCase( aChar )

Consider this implementation. Notice that, other than the decorator boilerplate, we really just have to write an if statement to preprocess the character before passing it on. Notice, too, that we have to cast the int read to char, so that the Character class can process it, and then cast it back in order to return it.

I tested my solution using a main() method just like the CatLoop we wrote for our opening exercise. All I did was use a LowerCaseInputStream to decorate either standard input or an input file. Because a LowerCaseInputStream is an InputStream, I could plug it into the source variable and use exactly the same processing code as before. That is polymorphism at work.


A Quick Exercise

Now it's your turn...

Write a Java class named CensorInputStream.
A CensorInputStream knows how to change all instances
of one character into another.

Suppose that we needed to filter a file, replacing all '+' characters with '-'s:

    math-cs > javac Censor.java            // uses an instance of our class
    math-cs > cat input.txt
    (1 + 2 + ((3 * 4 - 6 / 7) + 2 + 4) * 5)

    math-cs > java Censor input.txt + -
    (1 - 2 - ((3 * 4 - 6 / 7) - 2 - 4) * 5)

Of course, make your CensorInputStream a FilterInputStream.


A Quick Solution

Consider this possible solution. Extending FilterInputStream does most of the work of this exercise!!

CensorInputStream must provide two methods, read() and a constructor. What makes CensorInputStream different is that it must remember some of its own information in order to do its job: the character to be censored, and the character to be returned in its place. This means that we have to write a "real" constructor, not one that simply invokes "super( in );"

As with our LowerCaseInputStream, the read() here is little more than an if statement that preprocesses the character before passing it on.

Again, we can test our solution using a simple variation of the echo program. All I added was some processing of command-line arguments, so I can pass in a file name and the censor characters.

This example isn't all that contrived. Since the beginning of Internet time, rot13 has played an important role as a simple and reversible encoding mechanism for text. We can implement a Rot13InputStream quite easily using our decorator idea. And Java itself provides a censor-like FilterInputStream as a part of its javax.crypto package: CipherInputStream.


Polymorphic Streams

All of these wonderful examples depend on the same central idea: Java's stream classes are designed to be substituted for one another, using polymorphic variables. Without polymorphism, we would have to write more code, and more complicated, in many, many programs. With polymorphism, we instead write and use a set of stream classes that do just one thing, simply and well, and then plug them together to create smart objects to help us with our tasks. With smart objects, the tasks we try to do can often be boiled down to very simple ideas. The example we saw this week was the idea of echoing a file from one location to another. That simple little four- or five-line snippet worked over and over for us, using different objects each time.

That, my friends, is the power of polymorphism.


Wrap Up


Eugene Wallingford ..... wallingf@cs.uni.edu ..... April 14, 2005