Sun Java Solaris Communities My SDN Account Join SDN
 
Article

Writing Your Own Java I/O Stream Classes

 
 

Articles Index

I/O Stream Classes

The Java 2 Platform provides several classes for reading and writing streams of data. While most of these classes can be found in the java.io package, they are used in various other APIs such as java.net, java.util, java.sql, java.lang, java.security, JFC, CORBA, and others.

This article discusses the concept of writing specialized stream classes that can process (filter) data in a special fashion. Note, this is an advanced topic on I/O streams and assumes the reader has some knowledge of how I/O streams work. For the basics of I/O stream programming, the difference between byte and character streams, and the concept of stream chaining, please refer to Programming with Java I/O Streams.

The Java platform provides several specialized stream classes, the majority of which exists in the java.io package. These classes provide a variety of functionality and they usually come in pairs, that is, one for reading data and the other for writing data. For example, the java.io.FileReader and java.io.FileWriter provide file reading and writing capabilities, while the java.io.StringReader and java.io.StringWriter provide the ability to use java.lang.String as a source for reading data from and writing data to.

Java Stream Classes

Before you decide to write your own specialized stream classes, it might be worth your while inspecting the stream classes provided with the Java 2 platform. Because, one, there might already be a stream class available for your purpose, and two, you can use the source and documentation as examples for writing your own.

To give you a general idea of the various streaming classes available in the Java platform, the following is a complete list (excluding deprecated classes) of stream classes available in the java.io package.

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter
  • ByteArrayInputStream
  • ByteArrayOutputStream
  • CharArrayReader
  • CharArrayWriter
  • DataInputStream
  • DataOutputStream
  • FileInputStream
  • FileOutputStream
  • FileReader
  • FileWriter
  • FilterInputStream
  • FilterOutputStream
  • FilterReader
  • FilterWriter
  • InputStream
  • InputStreamReader
  • LineNumberReader
  • ObjectInputStream
  • ObjectOutputStream
  • OutputStream
  • OutputStreamWriter
  • PipedInputStream
  • PipedOutputStream
  • PipedReader
  • PipedWriter
  • PrintStream
  • PrintWriter
  • PushbackInputStream
  • PushbackReader
  • RandomAccessFile
  • Reader
  • SequenceInputStream
  • StreamTokenizer
  • StringReader
  • StringWriter
  • Writer

Here are some additional stream classes available in the java.util.zip and java.util.jar packages:

  • CheckedInputStream
  • CheckedOutputStream
  • DeflaterOutputStream
  • GZIPInputStream
  • GZIPOutputStream
  • InflaterInputStream
  • ZipInputStream
  • ZipOutputStream
  • JarInputStream
  • JarOutputStream

For more information on the above classes, please refer to the Java 2 platform documentation. Additionally, you can download Java 2 software and unbundle the src.jar to look at the source for the above classes.

Why Write Your Own Stream Classes?

With such a variety of I/O stream classes available in the Java platform, you might wonder why anyone would ever need to write their own stream classes. Well, there could be several reasons why, but mainly because your application might require processing a data stream in a special way for which the Java 2 platform does not have sufficient classes. The following are candidates for specialized stream classes.

  • Accessing Special Storage Devices
    With so many storage devices available on the market (such as tape and zip), there might be a need to access them from a Java application.
  • Spell Checker
    Java applications could use specialized stream classes to spellcheck the data in a stream and get back a list of misspelled words.
  • Language Converter
    A stream subclass could be written to convert words in an input or output stream from one language to another.
  • Text Search Screening
    Specialized stream classes could be used to search for specific words and obtain their locations in a data stream. Examples of uses of such classes could include a find feature in a text, editor or the ability to block out certain content (for example, adult content) in a proxy server.

How to Write Specialized Stream Classes

Writing your own stream classes is relatively simple. For reading character streams, you basically extend the java.io.Reader class (or java.io.FilterReader); for writing character streams, you extend the java.io.Writer class (or java.io.FilterWriter). For byte streams, you extend the java.io.InputStream (or java.io.FilterInputStream) for reading streams and java.io.OutputStream (or java.io.FilterOutputStream) for writing streams.

Note, to understand the difference between byte and character streams, please refer to the article, Programming with Java I/O Streams.

Filter Versus Top-Level Stream Classes

When developing specialized stream classes, you can choose to either directly subclass the top-level classes (InputStream, OutputStream, Reader, Writer) or you can subclass the Filter classes (InputFilterStream, OutputFilterStream, FilterReader, FilterWriter).

The filter stream classes seem a bit superfluous at times. However, they were originally designed for convenience, because they provide default implementations for all the methods found in the top-level classes. This enables you to override only those methods you need to. For example, if you extend OutputStream direct, you need to override the flush() and close() methods even though they are not abstract methods. This is necessary because these methods have empty bodies in the OutputStream class. On the other hand, when you extend the FilterOutputStream class, you generally end up providing implementations for two write() methods, versus one if you extended OutputStream direct.

Developing Specialized Byte Stream Classes

To develop specialized character stream classes, you either subclass the Reader class for reading data, or the Writer class for writing data. To develop a Reader subclass, you must override and provide implementations for the following methods:

  • int read(char[] cbuf, int off, int len)
  • void close()

To develop a Writer subclass, you must override and provide implementations for the following methods:

  • void write(char[] cbuf, int off, int len)
  • void close()
  • void flush()

Developing Specialized Byte Stream Classes

To develop specialized character stream classes, you either subclass the InputStream class for reading data or the OutputStream class for writing data. To develop an InputStream subclass, you must override and provide implementations for the following method:

   int read()

To develop an OutputStream subclass, you must override and provide implementations for the following method:

 
  void write(int b)

Specialized Stream Classes Outside the Java Platform

Now that you have a general idea of the variety of situations stream classes can be written for, it would help to look at some concrete examples of specialized non-Java stream classes.

Examples

The following two examples, TeeOutputStream and CountReader, provide simple illustrations of writing specialized stream classes. Later in this section, you will see a more robust, real-world example using Divya's BackOnline commercial application.

TeeOutputStream

The following class, TeeOutputStream provides an example of an OutputStream subclass. The purpose of this class is not only to write the bytes to the chained output stream, but also to write them to another output stream, such as the System.out device or a trace/debug file, for debugging purposes. The functionality provided by this class is similar to the UNIX tee command.

The code should be fairly easy to follow. Basically, this class overrides the write(int c), flush() and close() methods of its parent class, OutputStream. Additionally, this class provides a main() method for "self-testing" this class. The write() method performs the task of writing the data to two streams.

 
  import java.io.* ;

  
  public class   TeeOutputStream
         extends OutputStream
  {
      OutputStream tee = null, out = null;
  
  
     public TeeOutputStream(OutputStream chainedStream, 
                             OutputStream teeStream)
      {
           out = chainedStream;
  
           if (teeStream == null)
               tee = System.out;
           else
               tee = teeStream;
      }
      
  
  /**
  * Implementation for parent's abstract write method.  
  * This writes out the passed in character to the both,
  * the chained stream and "tee" stream.
  */
       
      public void write(int c) throws IOException
      {
           out.write(c);
  
           tee.write(c);
           tee.flush();
      }
  
  
      /**
       * Closes both, chained and tee, streams.
       */
      public void close() throws IOException
      {
           flush();
  
           out.close();
           tee.close();
      }
  
  
   /**
   * Flushes chained stream; the tee stream is flushed 
   * each time a character is written to it.
   */
      public void flush() throws IOException
      {
           out.flush();
      }
  
  
  
   /** Test driver */
   public static void main(
     String args[]) throws Exception
      {
          FileOutputStream fos =
                new FileOutputStream("test.out");
          TeeOutputStream  tos =  
                new TeeOutputStream(fos, System.out);
          PrintWriter      pw  =  
          new PrintWriter(new OutputStreamWriter(tos));
  
           pw.println("Testing line 1");
           pw.println("Testing line 2");
  
           pw.close();
      }
  }
  
  

CountReader

The following class, CountReader, provides an example of a Reader subclass. This class counts the number of characters and lines in an input stream of characters. At any point in reading the stream, the get*Count() methods can be used to obtain the character, word, and, or line count for the data read up to that point. The functionality provided by this class is similar to the UNIX wc command.

Like the TeeOutputStream class above, this class should also be easy to follow. The key method in this example is read(), which handles the task of counting characters, words, and lines.

 

   import java.io.* ;

    public class   CountReader
           extends Reader
    {
        Reader in = null;
        int read = 0, charCount=0, wordCount = 0,
           lineCount = 0;
        boolean whiteSpace = true;
    
    
        public CountReader (Reader r)
        {
            in = r;
        }
        
        
    /**
    * Implementation for parent's read method.  Counts 
    * chars, words, and lines.
    */
          
        public int read(char[] array, int off, int len) 
                   throws IOException
        {
            if (array == null)
                throw new IOException("Null array");
    
            // Do actual read
            read = in.read(array, off, len);
    
            // Now count
            charCount += read;  
            
            // Increment character count
            char c;
    
            for (int i=0; i < read; i++)
            {
               c = array[i];
    
               // Line count
               if (c == '\n')
                   lineCount++;
    
               // Word count
               if (Character.isWhitespace(c))
                   whiteSpace = true;
               else
               if (Character.isLetterOrDigit(c) 
                            && whiteSpace)
               {
                   wordCount++;
                   whiteSpace = false;
               }
           }
    
           return read;
        }
    
     
        public void close() throws IOException
        {
           in.close();
        }
    
    
        public int getCharCount() { return charCount; }
        public int getWordCount() { return wordCount; }
        public int getLineCount() { return lineCount; }
    
    
    
    /** Test driver */
    public static void main(
      String args[]) throws Exception
        {
           CountReader cr = new CountReader(new 
                       FileReader("CountReader.java"));
           char c[]       = new char[4096];
           int read       = 0;
     
           while ((
             read = cr.read(c, 0, c.length)) != -1)
              System.out.print(new String(c, 0, read));
    
           cr.close();
    
           System.out.println(
               "\n\nRead chars: " + cr.getCharCount() +
               "\n     words: " + cr.getWordCount() +
               "\n     lines: " + cr.getLineCount());
        }
    
    }
    
    

A Real World Example: Divya's 100% Pure Java Backup Application

Divya's 100% Pure Java applet and application, BackOnline, is a Internet-based backup, briefcase, and archival application that backs up a user's data files from the client machine to a server, as portrayed in the following illustration.

Figure 1

BackOnline makes extensive use of streams: everything from File Input/Output streams, to Socket streams, to its own specialized Protocol and Encryption/Decryption streams. When a user backs up a file, or files, in BackOnline, the file is processed via a compression stream, then an encryption stream, and then sent over the wire on a socket stream. The server receives this data using socket streams, and stores it in a file using file streams. For restoring files, this process simply works in reverse. The streams in use during the backup and restore processing are shown in the following figure.

Figure 2

Although, most of the stream classes used in BackOnline are ones available in the Java core APIs (such as socket, file, GZIP compression), there are two pairs of specialized stream classes written by Divya: one for the protocol used by the client and server to communicate with each other; the other for encrypting and decrypting the stream using 56-bit DES encryption.

Protocol Stream Classes

The application uses a protocol almost identical to HTTP version 1.1. Each client request (such as file backup) contains a header followed by a blank line, followed by the data for that request. The stream classes automatically handle the separation of the header and data portions in the stream, and provide the appropriate methods to obtain the header values and read() methods to read the actual data.

Here is the Java source code for the ProtocolInputStream class, which is used for reading in the protocol stream. Notice how the read and close methods override their parent-stream class' methods.

Encryption Stream Classes

When a user logs into BackOnline, in addition to providing the userid and password for authentication, they provide an optional encryption key, like a second password, which is used to encrypt and decrypt the stream. In other words, the application uses a key/password-based encryption/decryption scheme. Because this key is always entered by the user and never physically stored anywhere, it provides an extremely secure mechanism for transmitting and storing the user's data.

Here is the Java source code for the CryptOutputStream class, which is used for writing an encrypted stream. Notice how the Write, Flush and Close methods override their parent-stream class' methods.

After looking at the source for the TeeOutputStream, CountReader, ProtocolInputStream, and CryptOutputStream classes, you should have a very good idea of how to write your own stream classes.

Summary

By now, you probably have an in-depth understanding of how the Java I/O stream classes work. However, it will still be worth your while to take a look at the Java 2 platform source code, and documentation for the various stream classes, especially the classes in the java.io package.

Remember, while I/O streaming is perhaps not the most glamorous subject compared to other Java technologies, it is in a sense the "central nervous system" of the Java platform, because it is used in so many APIs. Therefore, a thorough understanding of I/O stream classes is essential.

coffeecup

Anil Hemrajani is a senior consultant at Divya Incorporated, a consulting firm specializing in Java/Internet software solutions. Anil provides Java/Internet-based architecture, design and development solutions to Fortune 500 companies, and occasionally writes articles and speaks at conferences. He can be reached at anil@divya.com.