Check out the new USENIX Web site.
FeatureUSENIX

 

using java

Input and Output Streams

rao_prithvi

by Prithvi Rao
<prithvi+@kiwilabs.com>

Prithvi Rao is the founder of Kiwilabs, which specializes in software engineering methodology and Java training. He has worked on the development of the MACH OS and a real-time version of MACH, and he holds two patents resulting from his work on mobile robots.



Input and output are clearly important parts of any language. In fact it is very likely that one of the first features of any new language that a practitioner learns is how to do IO. An example of this is the well-known "Hello World" example, which relies on being able to pipe the characters to an output "stream." Further, the ability to perform IO is a crucial part of any serious debugging effort. The ubiquitous "printf" is probably the first debugging "tool" that programmers use. In Java, all IO is accomplished using streams. The application creates a stream (which is not unlike a UNIX file descriptor) and then performs operations on the contents of the stream. This article is a short tutorial on "streams" — the basis for IO in Java.

I'll begin by describing input and output streams and then present examples of using them to implement simple UNIX utilities such as cat and ls.

Input Streams

The class java.io.InputStream is the base class for creating input streams. We will look at details of this class.

The sources of some typical input streams are File, String, Array of Bytes, and Sockets.

This is an abstract class and so we must create a subclass (also called a derived class); in other words, we cannot use it "as is." The methods in this abstract class describe the properties of the input stream. Subclasses of java.io.InputStream can override some or all of its methods.

Remember that all packages in Java will have exceptions associated with them, and java.io.InputStream is no exception (pardon the pun). In fact, almost all of the methods will throw IOException. The class definition will generally tell you when the exception will be thrown.

The first form of the input stream stores a byte (having read it from the stream) and places it in a buffer.

    public int read(byte[] b) throws IOException, NullPointerException

The buffer b must have been created with new; otherwise a null pointer exception results. This method tries to fill the buffer and returns the actual number of bytes read.

The second form of this method reads len bytes into the buffer starting at the position specified by offset.

    public int read(byte[] b, int offset, int len) throws IOException,
       NullPointerException, IndexOutOfBoundsException

Notice that the last exception is raised if reading takes it out of bounds.

Another useful method that is part of this package is skip. It attempts to skip n bytes. It may not always be able to do this (for instance, if EOF is reached).

    public long skip(long n) throws IOException

It discards the bytes skipped, where n is the number of bytes to be skipped, and it returns the actual number of bytes skipped.

It is also possible to query the input stream for available bytes. The following method returns the number of available bytes from the input stream.

    public int available() throws IOException

The interesting thing about this method is that the next caller can expect to read that number of bytes returned from available without blocking.

There is a way to "remember" all bytes read after marking an input stream.

    public synchronized void mark(int readlimit) throws IOException

If the markSupported method returns true, then mark remembers the stream at the position when mark is called. It then remembers everything that is read from that position in the stream. It is now ready to resupply those same bytes whenever the reset method is called.

However, if the application reads more than readlimit bytes before the call to reset is made, there is no guarantee that it will supply all the bytes again.

Let us consider the cases when markSupported returns true and false.

    public boolean markSupported()

Suppose it returns true. Then reset might throw an IOException if mark has never been set or has not been set since the last call to reset. It might throw an IOException if more than readlimit number of bytes have been read since the last call to mark. If no IOException is thrown, then the stream is reset to again supply (to any thread) all bytes read since the last call to mark.

Suppose it returns false. Then a call to reset might throw an IOException. If no IOException is thrown, then the stream is reset to a fixed state that depends on the type of input stream and how it was created. The bytes supplied after this to any caller of read depends on the type of input stream.

In general, marking streams is useful for things like parsers that have to look ahead to see what is in the stream. If the parser doesn't like what it gets, it might throw an exception and try something else with the bytes being supplied again.

Output Streams

The base class for output streams is java.io.OutputStream.

The methods of this class virtually mirror those of the input stream. This abstraction makes it easier to remember the semantics of IO in Java.

    public abstract void write(int b) throws IOException

This writes one byte to an output stream. The byte written is the low-order bits of b. The 24 high-order bits are ignored. It can throw an IOException if the stream is closed.

The second way of writing to the output stream is the following:

    public void write(byte[] b, int off, int len) throws IOException,
        NullPointerException, IndexOutOfBoundsException
This method writes len bytes from b starting from off. It throws an IOException if len + off exceeds length of the buffer. This method really loops writing a single byte at a time by calling write(int b). Since an IOException might be thrown during a write, there is no guarantee that all of the buffer will be written.

The output stream can be "flushed" using:

    public void flush() throws IOException

Examples using java.io.FileInputStream

This program creates a command that is similar to the UNIX command cat. The cat command takes a filename as argument and displays the contents of that file on the terminal.

    import java.io.*;

Import the io package, because that's where all the io classes are defined.

    public static void main(String[] args) throws IOException

As we have seen, many of the io methods throw IOException. We don't want to clutter the program with code for exception handling. Instead, we let main throw an IOException.

    InputStream ins = System.in;
    int abyte;

This is the variable for the input stream we will use. We initialize it to be the standard input. If no argument is supplied, this program will read System.in and then just echo whatever it reads.

    if (args.length == 1)
      {
        try
          {
            ins = new FileInputStream(args[0]);
          }
        catch(FileNotFoundException ex)
          {
            System.out.println("Cat: " + ex);
            System.exit(1);
         }
      }

If an argument has been provided on the command line, interpret it as a filename and try to open an input stream with that file as the source for the stream. If file is not found, we catch that exception, because that's what the UNIX cat command does too.

   while ((abyte = ins.read()) != -1)
      System.out.print((char)abyte);
   System.out.println();

The hard work is done and we've opened the stream. Now we just sit inside a loop and read one byte at a time, printing those bytes out as we go.

   if (ins != System.in)
     ins.close();

Close the stream when we are finished.

Example using java.io.File

This example demonstrates the use of java.io.File to build a very poor version of the UNIX command ls, which lists files in a directory.

This command will print the files in the current directory if no argument is supplied. If arguments are supplied, it treats those arguments as files or directories and lists them. This is part of the main program.

   if (args.length == 0)
     {
       filesOrDirs = new String[1];
       filesOrDirs[0] = new String(System.getProperty("user.dir"));
      }

If no argument is supplied, then the directory to list is the current directory. getProperty of the class java.lang.System contains many properties, one of which is the current directory.

   else
      {
       filesOrDirs = new String[args.length]; 
       System.arraycopy(args, 0, filesOrDirs, 0, args.length);
      }

Arguments have been supplied; copy them into an array for later use.

   for (int i = 0; i < filesOrDirs.length; i++)
      {
       ls(filesOrDirs[i]);
      }

Go into a loop that calls the method ls. ls does the hard work.

We now have to write the ls method.

   public static void ls(String fname)

We use a public static method so it could be called by anyone who imported this package (if we had put it in a package . . .). Keep in mind that fname is a string that is either the name of the current directory or a path.

   File f = new File(fname);

Create an instance of File representing the name fname.

   if (!f.exists())
      {
       System.out.println("Ls: " + fname + ": No such file or directory");
       return;
      }

exists is a method of java.io.File. Check to see if this file exists. If not, print message and return. We don't exit the program because that's not how the UNIX ls behaves. The UNIX version of ls continues even if some files don't exist.

   if (f.isFile())
      {
       System.out.println(fname);
      }

If this is an actual file, it prints the name.

   if (f.isDirectory())
      {
       String[] flist = f.list();
       File tmp;

If it's a directory, then the method list returns an array of strings containing the names of all files in the directory. So we now know that fname is a directory and we know all the files in that directory.

   for (int i = 0; i < flist.length; i++)
      {
       tmp = new File(fname, flist[i]);
       if (!tmp.exists())
        {
         System.out.println("Ls: " + flist[i] +
                ": No such file or directory");

         continue;
        }
       System.out.println(flist[i]);
      }

Sit in a loop, create a new File object, check that the file exists, and print out the name.

Conclusion

Clearly, the ability to do IO is very important in Java. We have shown how to manipulate a very small part of the overall capability. The beauty of using streams is that once the application has opened a stream to either read or write, the semantics are the same regardless of what is being read from or written to. This makes writing network applications, for example, much simpler than one might suppose.

In future articles we will explore this package further. An interesting exercise for readers is to implement other UNIX-type capabilities to enhance their understanding of this powerful package.

 

?Need help? Use our Contacts page.
Last changed: 18 Nov. 1999 mc
Using Java index
;login: index
USENIX home