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

Chapter 1. Streams

This chapter discusses Java’s stream classes, which are defined in the java.io.* package. While streams are not really part of RMI, a working knowledge of the stream classes is an important part of an RMI programmer’s skillset. In particular, this chapter provides essential background information for understanding two related areas: sockets and object serialization.

The Core Classes

A stream is an ordered sequence of bytes. However, it’s helpful to also think of a stream as a data structure that allows client code to either store or retrieve information. Storage and retrieval are done sequentially—typically, you write data to a stream one byte at a time or read information from the stream one byte at a time. However, in most stream classes, you cannot “go back”—once you’ve read a piece of data, you must move on. Likewise, once you’ve written a piece of data, it’s written.

You may think that a stream sounds like an impoverished data structure. Certainly, for most programming tasks, a HashMap or an ArrayList storing objects is preferable to a read-once sequence of bytes. However, streams have one nice feature: they are a simple and correct model for almost any external device connected to a computer. Why correct? Well, when you think about it, the code-level mechanics of writing data to a printer are not all that different from sending data over a modem; the information is sent sequentially, and, once it’s sent, it can not be retrieved or “un-sent.”[3] Hence, streams are an abstraction that allow client code to access an external resource without worrying too much about the specific resource.

Using the streams library is a two-step process. First, device-specific code that creates the stream objects is executed; this is often called “opening” the stream. Then, information is either read from or written to the stream. This second step is device-independent; it relies only on the stream interfaces. Let’s start by looking at the stream classes offered with Java: InputStream and OutputStream.

InputStream

InputStream is an abstract class that represents a data source. Once opened, it provides information to the client that created it. The InputStream class consists of the following methods:

public int available(  ) throws IOException
public void close(  ) throws IOException
public void mark(int numberOfBytes) throws IOException
public  boolean markSupported(  ) throws IOException
public abstract int read(  ) throws IOException
public int read(byte[] buffer) throws IOException 
public int read(byte[] buffer, int startingOffset, int numberOfBytes) throws
    IOException
public void reset(  ) throws IOException
public long skip(long numberOfBytes) throws IOException

These methods serve three different roles: reading data, stream navigation, and resource management.

Reading data

The most important methods are those that actually retrieve data from the stream. InputStream defines three basic methods for reading data:

public int read(  ) throws IOException
public int read(byte[] buffer) throws IOException
public int read(byte[] buffer, int startingOffset, int numberOfBytes) throws
    IOException

The first of these methods, read( ), simply returns the next available byte in the stream. This byte is returned as an integer in order to allow the InputStream to return nondata values. For example, read( ) returns -1 if there is no data available, and no more data will be available to this stream. This can happen, for example, if you reach the end of a file. On the other hand, if there is currently no data, but some may become available in the future, the read( ) method blocks. Your code then waits until a byte becomes available before continuing.

Tip

A piece of code is said to block if it must wait for a resource to finish its job. For example, using the read( ) method to retrieve data from a file can force the method to halt execution until the target hard drive becomes available. Blocking can sometimes lead to undesirable results. If your code is waiting for a byte that will never come, the program has effectively crashed.

The other two methods for retrieving data are more advanced versions of read( ), added to the InputStream class for efficiency. For example, consider what would happen if you created a tight loop to fetch 65,000 bytes one at a time from an external device. This would be extraordinarily inefficient. If you know you’ll be fetching large amounts of data, it’s better to make a single request:

byte buffer = new byte[1000];
read(buffer);

The read(byte[] buffer) method is a request to read enough bytes to fill the buffer (in this case, buffer.length number of bytes). The integer return value is the number of bytes that were actually read, or -1 if no bytes were read.

Finally, read(byte[] buffer, int startingOffset, int numberOfBytes) is a request to read the exact numberOfBytes from the stream and place them in the buffer starting at position startingOffset. For example:

read(buffer, 2, 7);

This is a request to read 7 bytes and place them in the locations buffer[2], buffer[3], and so on up to buffer[8]. Like the previous read( ), this method returns an integer indicating the amount of bytes that it was able to read, or -1 if no bytes were read at all.

Stream navigation

Stream navigation methods are methods that enable you to move around in the stream without necessarily reading in data. There are five stream navigation methods:

public int available(  ) throws IOException
public long skip(long numberOfBytes) throws IOException 
public void mark(int numberOfBytes) throws IOException 
public  boolean markSupported(  ) throws IOException 
public void reset(  ) throws IOException

available( ) is used to discover how many bytes are guaranteed to be immediately available. To avoid blocking, you can call available( ) before each read( ), as in the following code fragment:

while (stream.available(  ) >0 )) {
	processNextByte(stream.read(  ));
}

Warning

There are two caveats when using available( ) in this way. First, you should make sure that the stream from which you are reading actually implements available( ) in a meaningful way. For example, the default implementation, defined in InputStream, simply returns 0. This behavior, while technically correct, is really misleading. (The preceding code fragment will not work if the stream always returns 0.) The second caveat is that you should make sure to use buffering. See Section 1.3 later in this chapter for more details on how to buffer streams.

The skip( ) method simply moves you forward numberOfBytes in the stream. For many streams, skipping is equivalent to reading in the data and then discarding it.

Warning

In fact, most implementations of skip( ) do exactly that: repeatedly read and discard the data. Hence, if numberOfBytes worth of data aren’t available yet, these implementations of skip( ) will block.

Many input streams are unidirectional: they only allow you to move forward. Input streams that support repeated access to their data do so by implementing marking. The intuition behind marking is that code that reads data from the stream can mark a point to which it might want to return later. Input streams that support marking return true when markSupported( ) is called. You can use the mark( ) method to mark the current location in the stream. The method’s sole parameter, numberOfBytes, is used for expiration—the stream will retire the mark if the reader reads more than numberOfBytes past it. Calling reset( ) returns the stream to the point where the mark was made.

Tip

InputStream methods support only a single mark. Consequently, only one point in an InputStream can be marked at any given time.

Resource management

Because streams are often associated with external devices such as files or network connections, using a stream often requires the operating system to allocate resources beyond memory. For example, most operating systems limit the number of files or network connections that a program can have open at the same time. The resource management methods of the InputStream class involve communication with native code to manage operating system-level resources.

The only resource management method defined for InputStream is close( ). When you’re done with a stream, you should always explicitly call close( ). This will free the associated system resources (e.g., the associated file descriptor for files).

At first glance, this seems a little strange. After all, one of the big advantages of Java is that it has garbage collection built into the language specification. Why not just have the object free the operating-system resources when the object is garbage collected?

The reason is that garbage collection is unreliable. The Java language specification does not explicitly guarantee that an object that is no longer referenced will be garbage collected (or even that the garbage collector will ever run). In practice, you can safely assume that, if your program runs short on memory, some objects will be garbage collected, and some memory will be reclaimed. But this assumption isn’t enough for effective management of scarce operating-system resources such as file descriptors. In particular, there are three main problems:

  • You have no control over how much time will elapse between when an object is eligible to be garbage collected and when it is actually garbage collected.

  • You have very little control over which objects get garbage collected.[4]

  • There isn’t necessarily a relationship between the number of file handles still available and the amount of memory available. You may run out of file handles long before you run out of memory. In which case, the garbage collector may never become active.

Put succinctly, the garbage collector is an unreliable way to manage anything other than memory allocation. Whenever your program is using scarce operating-system resources, you should explicitly release them. This is especially true for streams; a program should always close streams when it’s finished using them.

IOException

All of the methods defined for InputStream can throw an IOException. IOException is a checked exception. This means that stream manipulation code always occurs inside a try/catch block, as in the following code fragment:

try{
	while( -1 != (nextByte = bufferedStream.read(  ))) {
		char nextChar = (char) nextByte; 
		...
	}
}
catch (IOException e) {
	...
}

The idea behind IOException is this: streams are mostly used to exchanging data with devices that are outside the JVM. If something goes wrong with the device, the device needs a universal way to indicate an error to the client code.

Consider, for example, a printer that refuses to print a document because it is out of paper. The printer needs to signal an exception, and the exception should be relayed to the user; the program making the print request has no way of refilling the paper tray without human intervention. Moreover, this exception should be relayed to the user immediately.

Most stream exceptions are similar to this example. That is, they often require some sort of user action (or at least user notification), and are often best handled immediately. Therefore, the designers of the streams library decided to make IOException a checked exception, thereby forcing programs to explicitly handle the possibility of failure.

Tip

Some foreshadowing: RMI follows a similar design philosophy. Remote methods must be declared to throw RemoteException (and client code must catch RemoteException). RemoteException means “something has gone wrong, somewhere outside the JVM.”

OutputStream

OutputStream is an abstract class that represents a data sink. Once it is created, client code can write information to it. OutputStream consists of the following methods:

public void close(  ) throws IOException 
public void flush(  ) throws IOException
public void write(byte[] buffer) throws IOException
public void write(byte[] buffer, int startingOffset, int numberOfBytes) throws
    IOException
public void write(int value) throws IOException

The OutputStream class is a little simpler than InputStream; it doesn’t support navigation. After all, you probably don’t want to go back and write information a second time. OutputStream methods serve two purposes: writing data and resource management.

Writing data

OutputStream defines three basic methods for writing data:

public void write(byte[] buffer) throws IOException
public void write(byte[] buffer, int startingOffset, int numberOfBytes) throws
    IOException
public void write(int value) throws IOException

These methods are analogous to the read( ) methods defined for InputStream. Just as there was one basic method for reading a single byte of data, there is one basic method, write(int value), for writing a single byte of data. The argument to this write( ) method should be an integer between 0 and 255. If not, it is reduced to module 256 before being written.

Just as there were two array-based variants of read( ), there are two methods for writing arrays of bytes. write(byte[] buffer) causes all the bytes in the array to be written out to the stream. write(byte[] buffer, int startingOffset, int numberOfBytes) causes numberOfBytes bytes to be written, starting with the value at buffer[startingOffset].

Tip

The fact that the argument to the basic write( ) method is an integer is somewhat peculiar. Recall that read( ) returned an integer, rather than a byte, in order to allow instances of InputStream to signal exceptional conditions. write( ) takes an integer, rather than a byte, so that the read and write method declarations are parallel. In other words, if you’ve read a value in from a stream, and it’s not -1, you should be able to write it out to another stream without casting it.

Resource management

OutputStream defines two resource management methods:

public void close(  ) 
public void flush(  )

close( ) serves exactly the same role for OutputStream as it did for InputStream—itshould be called when the client code is done using the stream and wishes to free up all the associated operating-system resources.

The flush( ) method is necessary because output streams frequently use a buffer to store data that is being written. This is especially true when data is being written to either a file or a socket. Passing data to the operating system a single byte at a time can be expensive. A much more practical strategy is to buffer the data at the JVM level and occasionally call flush( ) to send the data en masse.



[3] Print orders can be cancelled by sending another message: a cancellation message. But the original message was still sent.

[4] You can use SoftReference (defined in java.lang.ref) to get a minimal level of control over the order in which objects are garbage collected.

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