Java Programming   Java Programming
 |Sofia Home | Content Gallery |
Home
Syllabus
Schedule
Lessons
Assignments
Resources

Lesson 13.3 Wrappers

Stream Wrappers & Buffering

Reading and writing a byte at a time can be expensive in terms of processing time. Actually transmitting the byte doesn't take up much time, but the overhead of packing everything together, finding the byte on disk, and sending it to your program is the same whether you ask for one byte or for a thousand.

To counteract the high transportation cost of moving a single byte, Java can employ a strategy called buffering. Buffering means that whenever you ask for a byte, Java reads an entire block of values, and then stores those in "inventory" for when you need the next one. It never has to go back to your source and ask for more, unless it runs out.

As you saw in the last section, Java's FileInputStream can actually read all of the bytes in a file at once, by using byte arrays. Unfortunately, using byte arrays means you can't do the byte-by-byte processing that is required for applications such as filters. Instead, you have to read everything into memory, process it, and then write it back out. That can get really complicated.

Buffering makes your programs much more efficient because it cuts back on the transportation overhead of moving your data from source to sink. But it doesn't require you to give up byte-by-byte processing like using byte arrays does.

Back to Top

Buffered Streams

Java has two classes that provide buffered streams: the BufferedInputStream and the BufferedOutputStream class.

If you look at the constructor for BufferedInputStream, you might be surprised to see that there is no constructor that takes a file name as an argument. That's because buffering is a feature you may want to add to both file streams as well as network based streams.

Rather than creating specialized buffered streams for files and network connections, Java uses a general purpose class, and a technique called wrapping. Here's how wrapping works.

If you want to read from a file, you first construct a FileInputStream like this: 
FileInputStream fis = null;
try
{
   fis = new FileInputStream("Temp.txt");
  ...
}

This connects your input stream to a particular source. 

Temp.txt is connected to your program via the FileInputStream object named fis. Click image to enlarge.

To add buffering, you just pass your fully constructed FileInputStream as an argument to the BufferedInputStream constructor, like this: 
FileInputStream fis = null;
BufferedInputStream bis = null;
try
{
   fis = new FileInputStream("Temp.txt");
   bis = new BufferedInputStream(fis);
}

Now, your FileInputStream is "wrapped up" inside the BufferedInputStream. You get the best of both worlds:

The FileInputStream object named fis is wrapped inside the BufferedInputStream object named bis. Click image to enlarge.
Back to Top

Buffered vs UnBuffered

Let's take a look at an example to see the difference in speed supplied by buffering. Rather than using a GUI application, as we did with FileInputStreams, we'll use a pair of console-mode applications: Buffered.java and Unbuffered.java.

Here's the listing for Buffered.java. It simply opens and reads the file whose name is passed as args[0]. (If you run these programs on the Mac, remember to supply the arguments required to JBindery. If you don't supply a file name, the program doesn't do anything..

Before reading information, the current time is retrieved using the method:

time = System.currentTimeMillis();

As the file is read, the number of bytes in the file is counted. After reading the file, the currentTimeMillis() method is called again, and the elapsed time calculated and printed.

The Unbuffered application is identical except for a name change and the elimination of the BufferedInputStream bis:
Buffered.java
import java.io.*;

public class Buffered
{
  public static void main(String[] args)
  {
    if (args.length < 1)
    {
      System.err.println("java Buffered <file>");
      System.exit(1);
    }

    FileInputStream fis = null;
    BufferedInputStream bis = null;

    long time = 0;
    int ch, n = 0;
    try
    {
      fis = new FileInputStream(args[0]);
      bis = new BufferedInputStream(fis);

      time = System.currentTimeMillis();

      while ((ch = bis.read()) != -1)
            n++;
    }
    catch (Exception e)
    {
      System.err.println("OOPS!!!");
    }
    finally
    {
      try
      {
        if (fis != null) fis.close();
        if (bis != null) bis.close();
      }
      catch (IOException ie) { }
    }
    System.out.println("Buffered " + n + 
        "bytes in " +
        (System.currentTimeMillis() - time) +
        " milliseconds");
  }
}

Back to Top

The Results

Running the Unbuffered application on one of my computers against an 18K text file shows that the times are a relatively stable: 270-280 milliseconds. You can see the results here:
Running the Unbuffered Java program against a small text file in a DOS window.
The results running the Buffered application are also relatively stable at 60 milliseconds, unless the Buffered application is run immediately after the Unbuffered application, in which case its run-time drops to zero, as shown here.
Running the Buffered Java application against a small text file in a DOS window.
Is there a lesson to be learned from this?
  • Buffered I/O is faster than unbuffered, everything else being equal.
  • Everything else is seldom equal.

Binary Files

For data-processing type files, accounting records for instance, storing binary objects in human-readable form is wasteful, besides making it difficult to get your data back.

Here's an example. Suppose you want to store the number 157,235,225.75. 
  • To store this in human readable form [even assuming 1-byte characters], requires 12 bytes. 
  • When you want to read the number from disk, you have to read it as a String, and then convert the String to a format suitable for processing such as a float or double.
  • When it comes time to write it back to disk, you have to convert it from a float back into the textual form stored on disk.
A better solution is to store the information on disk exactly as it is stored in memory: to use binary disk files.
Back to Top

Binary Files and Java

This sounds like a real good idea until you remember that Java programs can run on many different platforms, and, the way that the Mac stores numbers in memory and that the way Windows machines store numbers in memory, is entirely different.

The Java solution is to create a cross-platform storage format and to provide two stream classes, DataInputStream and DataOutputStream, that will read and write these types of files no matter what platform you're reading or writing from.

Using Data Streams

You open a DataInputStream or a DataOutputStream the same way you do a BufferedInputStream, by wrapping.

If you want to create a file named "nums.dat" that holds binary information, you use DataOutputStream, along with a suitable FileOutputStream, like this: 

FileOutputStream fout = null;
DataOutputStream dout = null;

try
{
  fout = new FileOutputStream("nums.dat");
  dout = new DataOutputStream(fout);

  ..
}

Writing Data

Once you've constructed your stream, you write your data using the special I/O methods provided by the DataOutputStream class.

For every primitive type, there is a special method that replaces the generic "write-a-byte" method. Each of the primitive types has its own version of the write method.

For instance, to write a double to the DataOutputStream dout, you write:

dout.writeDouble(2.34);

Here's a short application, RandomDoubles.java, that writes 1000 doubles to the disk file named "nums.dat".
RandomDoubles.java
import java.io.*;

public class RandomDoubles
{
  public static void main(String[] args)
  {
    FileOutputStream fout = null;
    DataOutputStream dout = null;

    try
    {
      fout = new FileOutputStream("nums.dat");
      dout = new DataOutputStream(fout);

      for (int i = 0; i < 1000; i++)
       dout.writeDouble(Math.random());

      dout.close();
      fout.close();
    }
    catch (Exception e)
    {
      System.err.println("OOPS!");
    }
  }
}

Back to Top

The Results

When you run this program, 1,000 doubles are written to disk as binary numbers. The disk file thus takes up exactly 8,000 bytes as you can see here:

A DOS directory listing showing the file NUMS.DAT and its size.

If you open the file in DOS Edit, you'll see that the numbers are definitely not "human readable."

Viewing NUMS.DAT in DOS Edit. [Numbers appear as non-character symbols].

Reading Data

To get your information out, you simply replace the DataOutputStream dout with a DataInputStream, dis, replace the FileOutputStream with a  FileInputStream, and replace

dout.writeDouble();

with

d = dis.readDouble();

There is one additional complication, however. How do you tell when you've read all of your data.

In the earlier programs, the InputStream read() method returned a -1 when it reached end-of-file. With DataInputStream, however, that won't work, because -1 is a valid value. Instead, the DataInputStream class throws an EOFException which you should catch.

Here's the relevant piece of code from ReadRandom.java which reads and displays the file "nums.dat" created by the RandomDoubles application.
try
{
  fis = new FileInputStream("nums.dat");
  dis = new DataInputStream(fis);

  while (true)
  {
    System.out.println(dis.readDouble());
  }
}
catch (EOFException eoe)
{
  System.out.println("Terminated normally");
}

The program reads each of the random numbers written to "nums.dat" by Math.random() and prints them to the screen. When the last number is read, the EOFException is thrown and the program prints its "normal termination" message as you can see here:

Message printed at the end of the ReadRandom program, shown in a DOS window.
Back to Top

Something to Talk About

Is buffering really worth while? Do you want to find out? Then try this:
  • Download and compile the Buffered and Unbuffered applications.
  • Pick a large file around 200KB and run the two programs against the same file. 
What results do you get?

Please continue to the next section of this lesson.

 

Back to Top

 

Content Developed by Stephen Gilbert, Licensed under a Creative Commons License
Published by the Sofia Open Content Initiative
© 2004 Foothill-De Anza Community College District &The William and Flora Hewlett Foundation