O'Reilly logo

Java RMI by William Grosso

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Layering Streams

The use of BufferedInputStream illustrates a central idea in the design of the streams library: streams can be wrapped in other streams to provide incremental functionality. That is, there are really two types of streams:

Primitive streams

These are the streams that have native methods and talk to external devices. All they do is transmit data exactly as it is presented. FileInputStream and File-OuputStream are examples of primitive streams.

Intermediate streams

These streams are not direct representatives of a device. Instead, they function as a wrapper around an already existing stream, which we will call the underlying stream. The underlying stream is usually passed as an argument to the intermediate stream’s constructor. The intermediate stream has logic in its read( ) or write( ) methods that either buffers the data or transforms it before forwarding it to the underlying stream. Intermediate streams are also responsible for propagating flush( ) and close( ) calls to the underlying stream. BufferedInputStream and BufferedOutputStream are examples of intermediate streams.


close( ) and flush( ) propagate to sockets as well. That is, if you close a stream that is associated with a socket, you will close the socket. This behavior, while logical and consistent, can come as a surprise.

Compressing a File

To further illustrate the idea of layering, I will demonstrate the use of GZIPOutputStream, defined in the package java.util.zip, with the CompressFile application. This application is shown in Example 1-2.

CompressFile is an application that lets the user choose a file and then makes a compressed copy of it. The application works by layering three output streams together. Specifically, it opens an instance of FileOutputStream, which it then uses as an argument to the constructor of a BufferedOutputStream, which in turn is used as an argument to GZIPOutputStream’s constructor. All data is then written using GZIPOutputStream. Again, the main( ) method for this application is defined in the com.ora.rmibook.chapter1.CompressFile class.

The important part of the source code is the copy( ) method, which copies an InputStream to an OutputStream, and ActionListener, which is added to the Compress button. A screenshot of the application is shown in Figure 1-2.

The CompressFile application

Figure 1-2. The CompressFile application

Example 1-2. CompressFile.java

private int copy(InputStream source, OutputStream destination) throws IOException {
		int nextByte;
		int numberOfBytesCopied = 0;
		while(-1!= (nextByte = source.read(  ))) {
		destination.flush(  );
		return numberOfBytesCopied;

private class CompressFileAction extends AbstractAction {
	//  setup code omitted

	public void actionPerformed(ActionEvent event) {
		InputStream source = _startingFileTextField.getFileInputStream(  );
		OutputStream destination = _destinationFileTextField.getFileOutputStream(  );
		if ((null!=source) && (null!=destination)) {
 			try {
				BufferedInputStream bufferedSource = new BufferedInputStream(source);
				BufferedOutputStream bufferedDestination = new
				GZIPOutputStream zippedDestination = new
				copy(bufferedSource, zippedDestination);
				bufferedSource.close(  );
				zippedDestination.close(  );
			 catch (IOException e){}


How this works

When the user clicks on the Compress button, two input streams and three output streams are created. The input streams are similar to those used in the ViewFile application—they allow us to use buffering as we read in the file. The output streams, however, are new. First, we create an instance of FileOutputStream. We then wrap an instance of BufferedOutputStream around the instance of FileOutputStream. And finally, we wrap GZIPOutputStream around BufferedOutputStream. To see what this accomplishes, consider what happens when we start feeding data to GZIPOutputStream (the outermost OutputStream).

  1. write(nextByte) is repeatedly called on zippedDestination.

  2. zippedDestination does not immediately forward the data to buffered-Destination. Instead, it compresses the data and sends the compressed version of the data to bufferedDestination using write(int value).

  3. bufferedDestination does not immediately forward the data it received to destination. Instead, it puts the data in a buffer and waits until it gets a large amount of data before calling destination’s write(byte[] buffer) method.

Eventually, when all the data has been read in, zippedDestination’s close( ) method is called. This flushes bufferedDestination, which flushes destination, causing all the data to be written out to the physical file. After that, zippedDestination is closed, which causes bufferedDestination to be closed, which then causes destination to be closed, thus freeing up scarce system resources.

Some Useful Intermediate Streams

I will close our discussion of streams by briefly mentioning a few of the most useful intermediate streams in the Javasoft libraries. In addition to buffering and compressing, the two most commonly used intermediate stream types are DataInputStream/DataOutputStream and ObjectInputStream/ObjectOutputStream. We will discuss ObjectInputStream and ObjectOutputStream extensively in Chapter 10.

DataInputStream and DataOutputStream don’t actually transform data that is given to them in the form of bytes. However, DataInputStream implements the DataInput interface, and DataOutputStream implements the DataOutput interface. This allows other datatypes to be read from, and written to, streams. For example, DataOutput defines the writeFloat(float value) method, which can be used to write an IEEE 754 floating-point value out to a stream. This method takes the floating point argument, converts it to a sequence of four bytes, and then writes the bytes to the underlying stream.

If DataOutputStream is used to convert data for storage into an underlying stream, the data should always be read in with a DataInputStream object. This brings up an important principle: intermediate input and output streams which transform data must be used in pairs. That is, if you zip, you must unzip. If you encrypt, you must decrypt. And, if you use DataOuputStream, you must use DataInputStream.


We’ve only covered the basics of using streams. That’s all we need in order to understand RMI. To find out more about streams, and how to use them, either play around with the JDK—always the recommended approach—or see Java I/O by Elliotte Rusty Harold (O’Reilly).

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required