Session 26

Java I/O and Polymorphism


CS 2530
Intermediate Computing


Opening Exercise: Command Line Characters

(And I don't mean Neal Stephenson.)

Write a quick-and-dirty main program named CharacterCounter that displays the number of characters on the command line after the command. For example:

    > java CharacterCounter
    The command line contains 0 characters.

    > java CharacterCounter a
    The command line contains 1 character.

    > java CharacterCounter a bc def ghij
    The command line contains 10 characters.

Don't worry about that 's' until the end. (Ha!)



A Simple Solution

Here is a quick-and-dirty solution:

    int characterCount = 0;
    for ( int i = 0; i < args.length; i++ )
      characterCount += args[i].length();
    
    String ending = (characterCount == 1) ? " character." : " characters.";
    
    System.out.println( "The command line contains " +
                        characterCount + ending );

args are strings, so we can just add up their lengths. Java's computed if works nicely here.



Polymorphism in the Standard Packages

Last time, we saw another great example of polymorphism at work. With a polymorphic Ball variable, we could customize a MultiBallWorld by changing very little of its code, by plugging in a new kind of ball. We even used polymorphism to create our new balls, because the DeceleratingBall class had a polymorphic Ball instance variable, which allowed it to be customized without changing any of its code!

The Java libraries demonstrate the use of polymorphism, too. In particular, many of the graphical goodies we have learned use and depend on the ability to substitute one kind of thing for another. Classes such as TextArea and TextField have a lot of useful text editing behavior that is provided by default TextListeners, which remove a lot of low-level burden from the programmer.

Talking about anything as powerful and varied as polymorphism is not enough for most of us to understand the idea or how to use it. Talking about polymorphism won't help you learn Java much at all. Using these ideas in our programs, however, can help us learn some Java and understand better the benefits of polymorphism.

Today, we will consider Java's stream classes for I/O. Our goal is two-fold:

There is a symbiotic relationship. A lot of learning works that way.



Input Streams in Java

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

An input stream is a stream that serves as the source of data for a program. Java provides a number of different input streams in its standard java.io package. All of these classes extend the abstract class InputStream.

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

      public long skip( long n ) throws IOException {
        // The receiver discards n bytes.  May, for a variety
        // of reasons, skip a smaller number of bytes, even 0.
        // Returns the actual number of bytes skipped.
      }

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

      public void close() throws IOException { ... }

      public int read( byte[] b ... )        { ... }
        // These methods read chars into an array and return
        // the number of bytes read, or -1 if at end of stream

      // Also a set of methods for marking a spot in the stream
}

What is an abstract class? [FILL IN THE BLANK.]

Notice a nice feature of this class. The read() method is overloaded. But the more complex methods can be implemented in terms of the basic one-character-at-a-time method. This means that InputStream can provide method bodies for the complex methods, and each subclass has to override only read() to work for its specific source.

We can implement the skip(long n) method in terms of read(). How would you do it?

The basic loop would have to look something like this:

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

What happens if the stream contains fewer than n characters? The method throws an exception. Fortunately, that situation is easy to avoid:

    int nActual = Math.min( n, available() );
    for (int i = 0; i < nActual; i++)
      read();
    return nActual;

Notice how skip() uses the object's read() and available() methods. This sort of abstract design means that only a few of a class's methods need to know the actual details of the data of the class!

Designing classes in this way makes them easier to modify and extend. It also means we can create abstract classes that have no data at all -- yet still implement useful behaviors for future users of class.

This approach takes advantage of inheritance, substitutability, and polymorphism.



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 the program. Java provides four physical input stream classes in its standard package:

Each of these classes implements the read() method to return a byte, a kind of int. Many users of input streams cast this value as a char before using it (as a character). Some of these classes also implement or override other methods inherited from InputStream to do what makes sense for its data source.

A FileInputStream does just what you'd expect: read characters from a file. One way to open a file is to pass the file name to the constructor.

Here is an example that uses a FileInputStream to count the number of characters in a file:

    InputStream dataSource = new FileInputStream( args[0] );

    int totalCharacters = 0;
    while ( dataSource.read() != -1 )
      totalCharacters++;

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

We pass the name of the file as a command-line argument:

    > java CountBytes CountBytes.java
    401 bytes

What is an IOException? [FILL IN THE BLANK.]

Often in Unix, when the user does not pass (the name of) an input file as an argument, the program reads from standard input. In Java, standard input is an InputStream named System.in.

Can we modify CountBytes to reads from standard input whenever the user does not specify a filename? Certainly! Using a polymorphic variable means that we can even keep our change out of the code that does the real work:

    > java CountBytes
    slfnvs;kdfnkjsdf
    lrfghreogihe
    wefjwroigerwogi
    ^D
    46 bytes

Note that this code counts the end-of-line markers, too. If you would rather not do that, you can change it.

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

A StringBufferInputStream is a stream that reads characters from a String -- just as if it were a file!

    ...
    dataSource = new StringBufferInputStream( args[0] );
    ...

Now, the string we pass as a command-line argument is treated as the source of characters for the stream:

    > java CountBytes CountBytes.java
    15 bytes

The ability to read from strings as if they were files makes it possible load a file as string and read it without changing the code that does the work. This is a common technique when we need to optimize our program's I/O, or when we want to give our user the ability to create data on the fly.

(For completeness.)

A ByteArrayInputStream is similar to a StringBufferInputStream, but the characters come from -- you guessed it -- a byte[]. A PipedInputStream reads from a PipedOutputStream as if it were a file. This allows one program to generate a stream of data to be consumed by another program. The consumer reads data as if the characters come from a storage location. (Think of piping in Unix or DOS...)

Why even define PipedInputStream? To shield the client code from data representation.



Virtual Input Streams in Java

A virtual input stream is an input stream that uses another InputStream as its source of data. Whenever the user of a virtual input stream sends it a message, the virtual stream delegates most of the job to its helper stream. In particular, it delegates the reading of the next character to its helper.

What have we just described?

In Java, virtual input streams are implemented as decorators!

We can write our own virtual input stream in much the same way we wrote "virtual" Balls last time. Consider LowerCaseInputStream, which reads from another stream and returns only lowercase alphabetical characters. A LowerCaseInputStream will wrap an instance of InputStream:

    private InputStream dataSource;

... and take its helper as an argument to its constructor:

    public LowerCaseInputStream( InputStream s )
    {
      dataSource = s;
    }

... and implement read() by delegating to its helper. Of course, it will implement its own special behavior by processing the character before returning it:

    public int read() throws IOException
    {
      int c = dataSource.read();
      if ( 65 <= c && c <= 90 )
        return (int) Character.toLowerCase(c);
      return c;
    }

We can use a LowerCaseInputStream in the same way we did before:

    > java CountBytes CountBytes.java
    15 bytes

But we can also use it to rewrite a file in lowercase:

    > run LowerCaseDemo LowerCaseInputStream.java
    import java.io.inputstream;
    import java.io.ioexception;

    public class lowercaseinputstream extends inputstream
    {
    ...
        return c;
      }
    }

Many data processing tasks that can be done on a character-by-character basis can be done using a virtual stream of this sort. Once you start thinking this way, you'll be surprised how often this approach pops into your head.

Who knew that implementing a DeceleratingBall class would teach us how to write -- and understand -- input streams. The power of polymorphism and a cool design idea.



Wrap Up



Eugene Wallingford ..... wallingf@cs.uni.edu ..... November 27, 2012