Session 25

Polymorphism in the Stream


810:062
Computer Science II
Object-Oriented Programming


Opening Exercise

Write a quick-and-dirty main program named CountCharacters that displays the number of characters typed on the command line after the command, not counting the spaces.

For example:

    mac os x > java CountCharacter
    The command line contains 0 characters.

    mac os x > java CountCharacters a
    The command line contains 1 character.

    mac os x > java CountCharacters Eugene is a great guy!
    The command line contains 18 characters.


A Quick-and-Dirty Solution

Here is a simple job:

    public class CountCharacters
    {
        public static void main( String[] args )
        {
            int characterCount = 0 ;

            for ( int i = 0; i < args.length; i++ )
                characterCount += args[i].length();

            String suffix = (characterCount == 1) ? "" : "s";

            System.out.println( "The command line contains "  +
                                characterCount + " character" +
                                suffix         + "." );
        }
    }


Polymorphism in the Standard Packages

As noted last time, the java.awt package is chock-full of all kinds of graphical goodies. The capabilities of many of these classes (say, the text editing capabilities of TextArea provided by the default TextListener) are quite handy. They take a lot of burden off of the programmer to write code for common, low-level tasks.

The java.awt is also chock-full of examples showing how polymorphism can simplify interaction among objects and remove unnecessary details from the programmer's mind. For example, in Session 22, we saw how Panels can hold graphical Components for placement in a Frame. But a Panel is itself a Component, which means that Panels can be nested. And Panels and Frames both have layout managers that work in the same way, so programmers can organize components in the same way without worrying about which they are using.

Talking about polymorphism is not enough for us to understand what it is or how to use it, and talking about polymorphism won't help us learn Java much at all. Programming with these ideas will help you learn Java and help you understand the benefits of polymorphism.

This week, we study more of teh java.io package. In particular, we learn what we need to know to understand a piece of "magic" that we have been trusting since Session 10:

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

It turns out that we already know what we need to know -- we just don't know that we know. :-)


Input Streams in Java

Up until now, we have worked with readers and writers when doing input and output, respectively. Readers and writers are objects that know specifically how to work with text. But they are built out of objects that know how to work with any kind of characters.

We will now consider, these more basic objects, Java's stream classes. Our goal is two-fold:

That's how the world works sometimes.

A stream is a sequence of data. Many modern languages define their input and output (I/O) facilities in terms of input streams and output streams.

An input stream serves as the source of data for a program. For now, we will focus our study on input streams, but they work in much the same way as output streams.

Consider how a stream works:

a stream of values flows by...

Characters flow by, one at a time, and the program can access them in just this order. Once a character flows by, the program cannot access it again.

Java provides a number of different input streams, all of which extend the abstract class InputStream:

    public abstract class InputStream
    {
        public abstract int read() throws IOException
               // Subclasses must implement this method.

        public int read( byte[] b ) throws IOException
        {
            return read( b, 0, b.length);
        }

        public int read( byte[] b, int offset, int length ) throws IOException
        {
            // Uses a loop to read a single byte at a time
            // into b, using read().  Returns the number of
            // bytes read into the buffer, or -1.
        }

        public long skip( long n ) throws IOException
        {
            // The receiver discards n bytes.  May skip fewer
            // bytes, even 0. Returns the actual number skipped.
        }

        public int available() throws IOException
        {        // Subclasses should override this method.
            return 0;
        }

        public void close() throws IOException {}

        // ... a set of methods for marking and using
        //     a spot in the stream
    }

Notice some of the interesting features of this class:


A Quick Exercise

We can implement the skip( long n ) method in terms of read().

How?

The basic loop would have to look something like this:

    for (int i = 0; i < n; i++)
        read();

But what happens if the stream contains fewer than n characters? The read() method throws an exception. Fortunately, that is easy to fix:

    public long skip( long n )
    {
        int numberToRead = Math.min( n, available() );

        for (int i = 0; i < numberToRead; i++)
            read();

        return numberToRead;
    }


Designing Abstract Classes

skip() uses the object's read() and available() methods to implement its behavior. The more complicated read() methods use the object's basic read() to implement their behavior.

What is the value in writing most or all of the methods in InputStream in terms of read()?

This sort of abstract design means that only a few of the class's methods need to know about the class's instance variables.

Designing classes in this way makes them easier to use, extend, and modify. To make a new kind of InputStream, you really only have to implement a read() method and an available() method!

InputStream is a great design example for us. It shows us that we can implement abstract classes that have no data at all -- yet still implement useful behaviors for future clients!


Physical Input Streams in Java

A physical input stream is an input stream that reads directly from a real, actual, physical source in the machine or in a program.

Java provides five physical input stream classes. All of these classes override appropriate methods inherited from InputStream.

In particular, each defines read() to return a byte [an int], the ASCII value for a character. This value must be cast as a char before using it as a character.

The five physical input streams defined in java.awt are:

(The javax.sound.sampled package defines a sixth physical input stream, AudioInputStream, for working with bytes of audio data accessed from a file, stream or URL.)

You have some experience opening FileInputStreams, but not using them directly.


An Example of a FileInputStream

An instance of this class reads from a file on disk, accessed through its file name (a String).

Consider this simple example that counts all of the characters in a file.

    mac os x > java CountBytes0 CountBytes0.java
    425 bytes

    mac os x > wc CountBytes0.java
      17      46     425 CountBytes0.java

Because we don't need the characters we read, we just need to check that a read gives us a character at all. Thus, we can use dataSource.read() != -1 as our loop control.


A Quick Exercise

Modify CountBytes0 so that it reads from standard input whenever the user does not specify a filename.

All we need to do is change the line that initializes the dataSource variable:

        InputStream dataSource;

        if ( args.length == 0 )
            dataSource = System.in;
        else
            dataSource = new FileInputStream( args[0] );

Everything else stays the same. See this solution.

It's that simple,
with polymorphic variables
and substitutable classes.


An Example of a StringBufferInputStream

An instance of this class reads from a String as if it were a file!

Huh? Remember the definition of an input stream, given above: a sequence of characters that serves as the source of data for a program. A String is a sequence of characters, and a StringBufferInputStream lets us "read" from a String.

Consider this simple example that counts the characters in a string.

    mac os x > javac CountBytes2.java
    Note: CountBytes2.java uses or overrides a deprecated API.
    Note: Recompile with -deprecation for details.

    mac os x > java CountBytes2 CountBytes2.java
    16 bytes

Nothing else changes from CountBytes0 or CountBytes1, just the value we store in the dataSource variable. That, my friends, is polymorphism at work.


Factoring Out the Common

The only problem with CountBytes0, CountBytes1, and CountBytes2 is that they duplicate the processing loop. We'd like to isolate repeated code at a single place in our program. And main() methods that get very big at all almost always mask an object that we' like to use. So let's factor out a class that counts characters in an InputStream:

    public class StreamAnalysis
    {
        private InputStream dataSource;

        public StreamAnalysis( InputStream in )
        {
            dataSource = in;
        }

        public int charCount() throws IOException
        {
            int totalCharacters = 0;
            while ( dataSource.read() != -1 )
                totalCharacters++;
            return totalCharacters;
        }
    }

Now, our CountBytesX programs just need to choose their streams and hand them to a StreamAnalysis object. See, for example, CountBytes2.


An Exercise That Twists Us

Suppose that we wanted to modify CountBytes2 so that it could accept any number of command-line arguments.

What should we do?

We could "do it by hand" and write something like this:

    public static void main( String [] args ) throws IOException
    {
        InputStream dataSource;
        int         totalCharacters = 0;

        for ( int i = 0; i < args.length; i++ )
        {
            dataSource = new StreamAnalysis(
                             new StringBufferInputStream(args[i]) );
            totalCharacters += dataSource.charCount();
        }

        System.out.println( totalCharacters + " bytes" );
    }

But this requires us to write our own counting code: create a variable, initialize it, and increment it. StreamAnalysis objects already do that for us.

Even worse: what about the option to read from standard input? Do we need to make a special case to handle stdin separately from the String arguments we might receive?

The sky is dark...

Java offers us a better solution: a composite stream. Do you remember the Composite Pattern, which we learned about in Session 22? A Panel holds Components but is itself a Component, which allows us to nest panels.

The SequenceInputStream class is a composite: it holds multiple InputStreams and treats them as an uninterrupted sequence of characters. But it, too, is an InputStream, so we can use it in any code that expects an InputStream.

CountBytes3b.java demonstrates how to use a SequenceInputStream. The only part we don't know yet is the elements() being sent to the Vector of StringBufferInputStreams. It turns out that SequenceInputStream's constructor won't take a Vector as an argument, but it will take a listing of a Vector's elements. We'll learn about this bit of Java next week.

But does it work?

    mac os x > java CountBytes3b Eugene is a great guy, and SequenceInputStreams are cool.
    49 bytes

    mac os x > java CountBytes3a Eugene is a great guy, and SequenceInputStreams are cool.
    49 bytes

It does! More of the power of polymorphism...


Wrap Up


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