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.
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.
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:
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");
}
} |
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:
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.
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.
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:
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!");
}
}
} |
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: If you open the file in DOS Edit, you'll see that the numbers are definitely not "human readable."
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
with
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:
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.
|