Java includes a well-designed collection of stream classes and interfaces that make up most of the java.io package. This chapter describes several uses for streams and how to take advantage of them in your Java applets and applications. Because streams in Java are an integral part of network programming, this chapter will establish a foundation that is built upon in Part IV, "Networking with Java."
At the heart of every computer program is the concept of exchanging data between two or more sources. This can be as simple as reading data from or writing data to a buffer within the same program or as seemingly complex as transferring data between two different processes running on different systems located on opposite sides of the world. The sources and destinations of data transfer are also diverse. On the typical personal computer, input devices include the keyboard and mouse, whereas output devices include the monitor and printer. Devices capable of both input and output include the disk drive, modem, and network interface card.
Streams were created to abstract the concept of exchanging information
between these various devices and provide a consistent interface
for programmers to interact with different sources of I/O in their
programs. The basic idea of a stream is that data enters one end
of a data channel in a particular sequence and comes out the other
end in the same sequence-a first-in-first-out (FIFO) scheme. The
very nature of streams is a perfect match for an object-oriented
language such as Java. In addition, the built-in Java stream classes
can be extended through inheritance to provide the same operations
as those used for the primitive data types for your own user-defined
types.
| NOTE |
Depending on the capability of the source or destination of a stream to process the data being requested or written in a timely manner, your program might encounter situations where it has to wait for a device to process a request. This side effect of stream processing is known as blocking and most often occurs when hardware devices are involved, such as disk drives or network connections. The good news is that Java's stream classes and methods were designed with blocking in mind. The effects of blocking are mitigated by checking the stream for blocking before reading or writing, using buffers or caches, or running stream I/O requests in threads so that blocking does not affect the rest of your program |
Java's implementation of streams begins with two abstract classes: InputStream and OutputStream. All specialized streams for input and output operations are derived from these two classes. In addition, java.io includes a few interfaces and support classes that as a group provide a powerful framework for your streaming needs.
InputStream includes several methods that provide the
basic input needs for all the input streams to come. Each of the
read() methods extract one or more bytes of data from
the stream and move the current position in the stream along according
to how many bytes are read. Each read() and skip()
method will also block until at least some data is available to
process. Table 13.1 lists the name and a brief description of
each public method.
| Method | Description |
| int read() | Reads a single byte of data from the stream. The return value is the byte read as an int or -1 if the end of the stream was reached. |
| int read(byte[]) | Reads data into an array of bytes. The number of bytes read is determined by the length of the byte array passed as an argument. The return value is the number of bytes read or -1 if the end of the stream was reached. |
| int read(byte[], int, int) | Reads data into an array of bytes starting at a specific location in the array for a certain number of bytes. The return value is the number of bytes read or -1 if the end of the stream was reached. |
| long skip(long) | Jumps over the specified number of bytes in the stream. The actual number of bytes skipped is returned from this method. Note that this implementation of skip() casts the long passed as a parameter to an int before the skip is performed. It is the job of derived classes to override the default implementation to support skipping larger increments. |
| int available() | Returns the number of bytes that can be read without blocking. |
| mark(int) | Positions a placeholder at the current position in the stream that can be returned to later by calling reset() and sets the maximum number of bytes that can be read before the mark is invalidated. Because not all input streams support marking, markSupported() should be called first to determine whether marking is supported by the current input stream if the type of stream is unknown at runtime. |
| boolean markSupported() | Returns an indication of the current input stream's capability to support marking. |
| reset() | Returns the current position in the stream to the location set in the previous call to mark(). If the number of bytes read since the last mark() has exceeded the limit, an IOException is thrown. |
| close() | Closes the input stream and releases any resources allocated by the stream. |
| NOTE |
Many of the methods in the java.io package throw exceptions when problems arise. Most stream exceptions are of the IOException variety, a class derived from Exception, which is used exclusively by java.io. Four other exception classes derived from IOException provide more specific types of exceptions. All the InputStream methods except mark() and markSupported() throw an IOException when I/O errors occur |
Like InputStream, the abstract class OutputStream
provides the fundamental methods used by all its derived stream
classes. Also like the InputStream read() methods,
the write() methods will block until the data is actually
written to the output object or device. An IOException
is thrown if any of the OutputStream methods encounter
an error. Table 13.2 lists the name and a brief description of
each public method.
| Method | Description |
| write(int) | Writes a single byte to the stream. |
| write(byte[]) | Writes the entire length of an array of bytes to the stream. |
| write(byte[], int, int) | Writes a portion of an array of bytes starting at a specific location in the array for a certain number of bytes. |
| flush() | If the output stream is buffered, this method will force any bytes in the buffer to be written to the stream. |
| close() | Closes the output stream and releases any resources allocated by the stream. |
Now that we have covered the primary base classes used by input and output streams, let's cover some concrete stream classes. First we'll discuss the input and output streams that can be used with objects in memory, byte arrays, and String objects.
Derived from InputStream, a ByteArrayInputStream object allows you to perform input stream style operations on an ordinary byte array. This can be valuable when you are working with a sequence of data in the form of a byte array and want to read single or multiple bytes, skip, or jump around within the array. Using streams in this fashion extends the built-in array access methods of subscripting by providing a more powerful and flexible framework.
ByteArrayInputStream offers two constructors: one that
takes a byte array as a parameter and the other that accepts a
byte array, an offset to the first byte to start reading, and
the length of the byte array as parameters.
| NOTE |
ByteArrayInputStream internally stores a reference to the byte array passed to the constructor as well as the length of the array. Therefore, the byte array is not duplicated within ByteArrayInputStream. However, be aware that any changes made directly to the byte array used by the ByteArrayInputStream will also affect the stream. Also, marking is not supported by this class, but the reset() method will reposition the stream to the beginning of the array |
The sample Java application found in Listing 13.1 illustrates how a byte array can be accessed as an input stream and the effect of manipulating the byte array after the ByteArrayInputStream object has been created.
Listing 13.1. EX13A.java.
import java.io.*;
class EX13A
{
void printInputStream(InputStream s)
{
try
{
System.out.print("[");
while (s.available() > 0)
{
System.out.print(s.read());
}
System.out.println("]");
s.reset();
}
catch (Exception e)
{
System.err.println("Error displaying array: " + e);
}
}
public static void main(String args[])
{
byte buf1[] = { 1, 2, 3, 4, 5 };
// Create an input stream based on buf1.
InputStream is1 = new ByteArrayInputStream(buf1);
EX13A example = new EX13A();
example.printInputStream(is1); // Print the stream as is.
buf1[0] = 0; // Modify the array outside of the stream.
example.printInputStream(is1); // Print it again.
byte buf2[] = { 6, 7, 8, 9 }; // Create another array...
buf1 = buf2; // ... and assign it to buf1.
example.printInputStream(is1); // Print it one last time.
// Create another input stream based on buf2 that works with
// the last two bytes of the array only.
InputStream is2 = new ByteArrayInputStream(buf2, 2, buf2.length - 2);
example.printInputStream(is2); // Finally, print is2.
}
}
This application produces the following output:
[12345] [02345] [02345] [89]
Notice that when the array used by the stream is modified after the stream is created, the change is reflected the next time the stream is printed. However, also note that when the array used by the stream is assigned to another array, the internal reference to the original array is unaffected.
This class is the complement of ByteArrayInputStream
in that it allows you to write data into an array of bytes. Except
with this output version, the class internally creates and maintains
the byte array for you. In fact, it even will expand the byte
array as needed. Of course, it also has methods to retrieve the
internal byte array at any point as a byte array, string, or Unicode
string.
| NOTE |
Although all strings in Java are represented in Unicode format, the toString(int) method in ByteArrayOutputStream allows you to specify the value used for the upper byte of each 2-byte character represented in the string |
The following simple code segment illustrates how to create, write to, and retrieve the contents of a ByteArrayOutputStream:
// Create a stream with a starting internal buffer size of 1 byte.
ByteArrayOutputStream os = new ByteArrayOutputStream(1);
try
{ // Overflow initial allocated buffer size.
os.write(72); // "H"
os.write(105); // "i"
}
catch (IOException e)
{
System.err.println("Error writing to byte stream: " + e);
}
// Retrieve stream's internal buffer as a byte array.
byte buf[] = os.toByteArray();
// See how it grew...
System.out.println("Length of array is " + buf.length + "\n" + os.toString());
Like ByteArrayInputStream, StringBufferInputStream allows you to interact with a memory-based object, namely a string, in the context of a stream. However, because the String class already provides a wealth of access methods, the use of StringBufferInputStream solely for working with strings does not add much value. But if your program is expected to handle data from conventional stream sources (for example, files or network connections) as well as strings, StringBufferInputStream provides the necessary abstraction.
Although file I/O in Java applets is usually denied or restricted due to security constraints imposed by browsers, the java.io package includes a complete set of stream classes for dealing with disk files. Indeed, Java applications are not subject to the same security constraints as applets and can include sophisticated file I/O functionality.
From the input side, FileInputStream provides basic input
stream capabilities for dealing with files. A FileInputStream
object can be created using one of three constructors that accept
the name of a file, a File object, or a FileDescriptor
object, respectively.
| NOTE |
Although File and FileDescriptor are classes within the java.io package, they will not be discussed in detail here because they are not part of the stream family of classes. Suffice it to say that the File class is used to represent a file in terms of the notation used by the host file system and provides basic file and directory functions such as making directories, enumerating files in a directory, deleting files, and renaming files. The FileDescriptor class is simply used to represent a handle to an open file or socket. Sockets and their integration with streams will be covered in detail in Part IV, "Networking with Java. |
The example in Listing 13.2 prints the contents of a text file named somefile.dat to the console.
Listing 13.2. EX13B.java.
import java.io.*;
class EX13B
{
public static void main(String args[])
{
try
{
// Create a simple file input stream.
FileInputStream fis = new FileInputStream("somefile.dat");
// Read the entire contents of the file.
while (fis.available() > 0)
{
System.out.print((char)fis.read());
}
}
catch (Exception e)
{
System.err.println("Error reading file: " + e);
}
}
}
The FileOutputStream class is very similar to FileInputStream
in that it can be created by either a filename passed as a string,
File object, or FileDescriptor.
| NOTE |
Because FileOutputStream starts writing at the beginning of a file, any data in the file before it is opened will be lost. If you would like to append data to a file or begin writing at a specific location, the RandomAccessFile class can be used in conjunction with FileOutputStream to achieve the same results as shown in the following example |
The example in Listing 13.3 opens a file for reading and writing using a RandomAccessFile object, binds an output stream to it, and writes a string to the end of the file.
Listing 13.3. EX13C.java.
import java.io.*;
class EX13C
{
public static void main(String args[])
{
RandomAccessFile rf;
try
{
// Open a file for read and write access.
rf = new RandomAccessFile("outfile.dat", "rw");
// Use the just opened file as the destination for a stream.
of = new FileOutputStream(rf.getFD());
// Attach a print stream to output stream.
PrintStream pos = new PrintStream(new FileOutputStream(rf.getFD()));
// Go to the end of the file...
rf.seek(rf.length());
// ... and write a string.
pos.print("Tack this to the end of the file.");
}
catch (IOException e)
{
System.err.println("Error writing to file: " + e);
}
}
}
As mentioned earlier, streams are simply a metaphor for data entering one end of a data channel and exiting the other end in the same order. What if you would like to modify the data as it travels in or out of the data channel, without having to extend Java's input and output streams? Filter streams not only provide the capability to manipulate data as it passes through a stream but they can also be chained together to create a combined effect of several filters. This is perhaps the most powerful aspect of Java's streams.
There are two base classes that input and output filter classes are derived from: FilterInputStream and FilterOutputStream.
On the input stream side, FilterInputStream is the base class for all the input stream filters. This class does not make any modifications to the attached stream, but merely provides the chaining functionality that will be exploited by its subclasses. The FilterInputStream is attached to an input stream by passing the input stream to the filter stream's constructor.
InputStream is = new FileInputStream("somefile.dat");
FilterInputStream fis = new FilterInputStream(is); // Meaningless filter attached.
The BufferedInputStream's contribution to its attached
stream is the addition of a buffer, or cache, to the stream. This
has the effect of improving the performance of some input streams,
especially those bound to slower sources like a file or network
connection.
| NOTE |
The C++ stream library uses the subclasses of the abstract streambuf class to provide buffering to its streams. However, buffered streams in C++ encapsulate a pointer to a streambuf object rather than using Java's filter approach |
You can specify the size of the buffer used by BufferedInputStream when it is created or accept the default of 2048 bytes. It then intercepts your calls to the read methods and attempts to satisfy your request with data in its buffer. If the data in the buffer is not sufficient, a read to the next input stream in the chain is done to fill the buffer and return the data requested. Anytime a read request can be satisfied from the buffer, you have saved a potentially slow read from the attached stream. BufferedInputStream also supports marking and ensures that the mark limit set remains valid.
Because file access can sometimes be quite slow, let's enhance the example in Listing 13.2 to use a BufferedInputStream. The improved version is shown in Listing 13.4.
Listing 13.4. EX13D.java.
import java.io.*;
class EX13D
{
public static void main(String args[])
{
try
{
// Create and attach a simple file input stream to a
// buffered filter, using the default buffer size of 2048 bytes.
BufferedInputStream bis = new BufferedInputStream(new
åFileInputStream("somefile.dat"));
// Now read the buffered stream.
while (bis.available() > 0)
{
System.out.print((char)bis.read());
}
}
catch (Exception e)
{
System.err.println("Error reading file: " + e);
}
}
}
Although it might not be obvious from this example, when the first call to read() is made, the BufferedInputStream reads in 2048 bytes (the default size of the buffer) and simply returns 1 byte at a time from the buffer for subsequent reads. There are certainly hardware and operating system caches involved as well, but it is always going to be faster to satisfy a read request from a memory location within the program than it is to make a system call to the operating system. It would also be more efficient to read more than one byte at a time from the stream, especially if it is unbuffered, but as we will see in the next section, some data types within a stream are only made up of a few bytes, so the value of BufferedInputStream still exists.
A stream of data, especially if it contains primitive data types, is of little use unless it can be converted from a sequence of bytes into its native type. Indeed, all the examples we have looked at thus far have dealt with streams of text data that required nothing more than casting each int returned to a character before it was used.
char c = (char)someinputstream.read();
In real programs, the data being transferred through streams is certainly going to be more complex and made up of several different types. And for most primitive data types such as long, float, and double, casting is not appropriate. The task of converting data to its native type as it is received is the perfect job for a filter stream. DataInputStream is a subclass of FilterInputStream and is responsible for filtering primitive data types from an input stream.
Because translating unknown pieces of machine-independent data into primitive data types has meaning outside of input streams, the DataInput interface was developed to create a generic connection to its translation methods. DataInputStream implements the DataInput interface.
Table 13.3 lists the name and description of each method of the
DataInput interface.
| Method | Description |
| readFully(byte[]) | Like read(byte[]), this method reads data into an array of bytes. The number of bytes read is determined by the length of the byte array passed as an argument. |
| readFully(byte[], int, int) | Like read(byte[], int, int), this method reads data into an array of bytes starting at a specific location in the array for a certain number of bytes. |
| int skipBytes(int) | Like skip(long), this method jumps over the specified number of bytes in the stream. The actual number of bytes skipped is returned from this method. Note that this skip() takes an int rather than a long as a parameter. |
| boolean readBoolean() | Reads and returns a Boolean. |
| byte readByte() | Like read(), this method reads and returns a single byte of data but returns the byte as a byte rather than an integer. |
| int readUnsignedByte() | Reads and returns an unsigned byte cast as a 32-bit integer to prevent possible truncation. |
| short readShort() | Reads and returns a 16-bit short. |
| int readUnsignedShort() | Reads and returns an unsigned short cast as a 32-bit integer to prevent possible truncation. |
| char readChar() | Reads and returns a 16-bit character. |
| int readInt() | Reads and returns a 32-bit integer. |
| long readLong() | Reads and returns a 64-bit long. |
| float readFloat() | Reads and returns a 32-bit float. |
| double readDouble() | Reads and returns a 64-bit double. |
| String readLine() | Reads a line of text until a \r, \n, or \r\n is found. The text is returned as a string. |
| String readUTF() | Like readLine(), this method reads a line of text from the stream. However, the text is read as a UTF-8 encoded string. |
| NOTE |
Because the built-in Java integer types are all signed, they might not be large enough to hold their unsigned counterparts. For this reason, readUnsignedByte() and readUnsignedShort() return integers to prevent the value from possibly being truncated. Remember that because an integer in Java is 32 bits, it is more than capable of holding the largest possible unsigned byte or unsigned short values |
Rather than include an example of how to use DataInputStream here, let's see how it works in conjunction with some of the other filter streams. The example for LineNumberInputStream (discussed next) and DataOutputStream both use DataInputStream.
The LineNumberInputStream is another input stream filter, except its task is to count the number of lines that have been read. It is most useful when you are reading several lines of text from a source, typically a file, and want to keep track of the current line number or total number of lines in a stream. The methods setLineNumber() and getLineNumber() enable you to set and get the internal line number count maintained by LineNumberInputStream.
Consider the example in Listing 13.5 that implements a simple text search program similar to UNIX's grep command.
Listing 13.5. EX13E.java.
import java.io.*;
class EX13E
{
public static void searchFile(String criteria, File file)
{
try
{
// Create a buffered line-number stream based on the target file.
LineNumberInputStream linestream =
new LineNumberInputStream
(new BufferedInputStream(new FileInputStream(file)));
DataInputStream dis = new DataInputStream(linestream);
while (dis.available() > 0)
{
String line = dis.readLine();
if (line.indexOf(criteria) != -1)
{ // Found a match!
System.out.println("File " + file.getName() + ", line " +
linestream.getLineNumber() + ":\n" + line);
}
}
}
catch (IOException e)
{
System.err.println("Error reading " + file.getName() + ": " + e);
}
}
private static void usage()
{
System.err.println("Usage: grep searchtext target");
System.err.println("Where target is a file or directory.");
}
public static void main(String args[])
{
if (args.length != 2)
{
System.err.println("Invalid number of arguments.");
usage();
return;
}
File target = new File(args[1]);
if (target.isDirectory())
{ // Retrieve a list of the files in the directory...
String[] list = target.list();
// ... and search each file for the search text.
for (int i = 0; i < list.length; i++)
{
searchFile(args[0], new File(target, list[i]));
}
}
else if (target.isFile())
{
// Just one file so just pass it to the search routine.
searchFile(args[0], target);
}
else
{
System.err.println("Target is not a file or directory.");
usage();
}
}
}
Notice how a BufferedInputStream is sandwiched between the LineNumberInputStream and the FileInputStream to provide I/O buffering for each file. It is critical that the BufferedInputStream be attached first so that all the other filter streams will leverage the buffering. It is also important that the LineNumberInputStream be attached before the DataInputStream so that calls to the DataInputStream are sent through the LineNumberInputStream. Otherwise, if LineNumberInputStream is attached to the DataInputStream, each line will not be counted as it is read. Because we want to read text from each file one line at a time, DataInputStream was used for its readLine() method.
This input filter stream is somewhat unusual in that it puts a
character back into an input stream. It can be used if you are
expecting a certain sequence of characters and want to test the
next byte in the stream to see whether it maintains the sequence.
If the sequence is broken, PushbackInputStream has an
unread() method that stages a byte to be returned as
part of the next read of the stream. The readLine() method
of DataInputStream uses a PushbackInputStream
(if the host stream is not already a PushbackInputStream)
if the carriage-return line-feed (\r\n) sequence is broken.
| NOTE |
The PushbackInputStream filter functions similar to the peek() member function of the iostream class in the C++ stream library. However, the PushbackInputStream returns a byte to the stream whereas the peek() member function in iostream allows you to preview the next character in the stream without removing it |
On the output stream side, FilterOutputStream forms the basis for the output stream filters. Like FilterInputStream, it does not make any modifications to stream data as it passes by but only provides the chaining used by its subclasses. The FilterOutputStream is attached to an output stream by passing the output stream to the filter stream's constructor.
BufferedOutputStream provides the same performance gains that BufferedInputStream does for input streams, but its buffer is used for an output stream. The basic concept is the same. Requests to write to an output stream are cached in an internal buffer. When the internal buffer reaches capacity it is flushed from the buffer to the output stream. The size of the internal buffer defaults to 512 bytes but can be overridden by calling the appropriate constructor with the desired size.
Because the use of a BufferedOutputStream is so similar to that of BufferedInputStream, an example will not be presented.
This filter provides methods that write primitive data types to an output stream. Primitive data types are most often used when working with files or network connections. Like DataInputStream, this class implements the methods of an interface, DataOutput.
The DataOutput interface defines the methods to write Java's primitive data types to a source. Within the java.io package, DataOutput is implemented by DataOutputStream and RandomAccessFile. As already mentioned, DataOutputStream implements DataOutput to write primitive types to an output stream. RandomAccessFile implements DataOutput to write primitive types solely to a disk file. Although a member of the java.io package, RandomAccessFile will not be discussed here because it is not a part of the stream hierarchy.
Table 13.4 lists the name and description of each method of the
DataOutput interface as it relates to DataOutputStream.
| Method | Description |
| write(int) | Writes 1 byte as a 32-bit integer. |
| write(byte[]) | Writes an entire byte array. The number of bytes written is determined by the length of the byte array passed as an argument. |
| write(byte[], int, int) | Writes the portion of a byte array starting at a specific location in the array for a certain number of bytes. |
| writeBoolean(boolean) | Writes an 8-bit Boolean. |
| writeByte(int) | Like write(int), this method writes a single byte as a 32-bit integer. |
| writeShort(int) | Writes a short as a 32-bit integer. |
| writeChar(int) | Writes a char as a 32-bit integer. |
| writeInt(int) | Writes a 32-bit integer. |
| writeLong(long) | Writes a 64-bit long. |
| writeFloat(float) | Writes a 32-bit float. |
| writeDouble(double) | Writes a 64-bit double. |
| writeBytes(String) | Writes a String as bytes. |
| writeChars(String) | Writes a String as chars. |
| writeUTF(String) | Writes a String as a UTF-8 encoded sequence of characters. |
| NOTE |
The C++ stream library uses operator overloading to write primitive data types to a stream. Although the syntax of operator overloading may appear more elegant, it can be difficult to program correctly and can be confusing to users of your classes. The creators of Java felt that this was too much of a price to pay |
The example shown in Listing 13.6 ties together several of the stream classes and stream filters that we have already discussed into one program. The basic idea of the program is to read statistics about itself when it is started, update the statistics, and write them back out. The statistics are stored in a file with the same name as the class and the extension .dat.
Listing 13.6. EX13F.java.
import java.io.*;
import java.util.Date;
class EX13F
{
private Date dateLastUpdated; // Date the last time the stats for this program
åwere written.
private String userLastUpdated; // User's name that performed the last update.
private int executionCount; // The number of times this program has been run.
// This method reads the program stats from a file by the same name as the class.
public void retrieveStats() throws IOException
{
try
{
// Create a data stream filter with a file input stream as the host
åstream.
DataInputStream dis = new DataInputStream(new
åFileInputStream(getClass().getName() + ".dat"));
// Initialize the data members with the values in the stat file.
dateLastUpdated = new Date(dis.readLong());
userLastUpdated = dis.readUTF();
executionCount = dis.readInt();
}
catch (FileNotFoundException e)
{
// Eat this exception since the file won't exist initially.
}
}
// This method writes the program stats back out to the same file.
public void saveStats() throws IOException
{
// Update date is now.
Date today = new Date();
// This time create an output data filter attached to a file output stream.
DataOutputStream dos = new DataOutputStream(new
åFileOutputStream(getClass().getName() + ".dat"));
// Write each value back out to the file.
dos.writeLong(Date.parse(today.toGMTString()));
if (userLastUpdated.length() == 0)
userLastUpdated = "Ace Programmer";
dos.writeUTF(userLastUpdated);
dos.writeInt(++executionCount);
}
public static void main(String args[])
{
EX13F example = new EX13F(); // Create an instance.
try
{ // Read the stats in from the last run.
example.retrieveStats();
// And print them out.
System.out.println("Program last update date: " +
åexample.dateLastUpdated);
System.out.println("Last updated by: " + example.userLastUpdated);
System.out.println("Execution count: " + example.executionCount);
// Now prompt the user for their name to update the stat file.
System.out.print("\nWhat is your name: ");
System.out.flush();
// Use a data stream again so we can use readLine()
// to read from the console.
DataInputStream stdin = new DataInputStream(System.in);
example.userLastUpdated = stdin.readLine();
// Finally, save the stats back out to the file.
example.saveStats();
}
catch (Exception e)
{
System.err.println("Error processing stats file: " + e);
}
}
}
In this example, primitive data types are read and written from a disk file using DataInputStream and DataOutputStream filters. The current date is converted to a long before being written to the file and is read back in as a long and converted back into a date. The user's name is written out as a String using writeUTF() to take advantage of the fact that this method writes the string's length to the stream prior to the actual characters. The readUTF() method then reads the stream for the size of the string first to determine the length of the string-a built-in variable length string handler. Finally, the execution count of the program is written and read as an integer.
This program also illustrates how to prompt for and retrieve user input from the keyboard using a text-based application. The java.lang.System class has three public static stream objects representing the standard input, output, and error for the system. The file system reserves file descriptors 0, 1, and 2 for standard input, output, and error, respectively. Although these three file descriptors are used most of the time for interacting with the keyboard and console, they can also be redirected to files and the standard input and output of other programs via unnamed pipes. Therefore, the System.in object is a buffered input file stream while System.out and System.err are buffered print output file streams. The host operating system's command interpreter, or shell, takes care of redirecting the standard input, output, and error in the appropriate context each time a program is run.
We have actually been using the PrintStream filter in every sample program in this chapter. As mentioned in the previous paragraph, the System.out and System.err objects are FileOutputStreams that use a PrintStream filter. This filter adds the capability to force the attached stream to be flushed every time a newline (\n) character is written. It also provides several overloaded versions of print() and println() that write each of the primitive data types to the attached stream as strings. In addition, each of the println() methods also append a newline to each object written.
Pipes have been an integral part of several operating systems for many years. In general, pipes are used to provide a one-way communication link between two processes. In Java, pipes can be used to connect two threads or two applets. And since pipes were implemented as streams, any of the stream filters can be attached to a pipe to provide the same effects that we have already seen. If a two-way communications link is needed, two pairs of pipes can be constructed that flow in opposite directions.
Pipes are supported by two classes: PipedInputStream and PipedOutputStream. They must be created in pairs and connected to each other by using either class's connect() method.
The receiving end of a piped stream is supported by PipedInputStream.
It contains a 1024-byte internal ring buffer that is used to hold
incoming data as it is received from its connected PipedOutputStream.
| NOTE |
Physically, a ring buffer is like any other buffer in memory. However, where it differs from other buffers is in how it is used. Two offsets are used to manage a ring buffer: one represents the position of the last byte written to the buffer and the other represents the position of the last byte read. Once either offset reaches the end of the buffer, it is moved back to the beginning. As long as the write offset stays equal to or ahead of the read offset, a ring buffer maintains the illusion of an endless piece of memory. However, if the write offset wraps around and passes the read offset, data can be lost. Fortunately, PipedInputStream will block further writes to its ring buffer by PipedOutputStream until data is read |
When data is written to an output pipe, PipedOutputStream
calls the receive() method of its attached PipedInputStream
to send data down the pipe. The receive() method will
write the data to its buffer if there is room. Otherwise, it will
block the output pipe until there is space in the buffer or the
pipe is broken. Likewise, when a read is performed on PipedInputStream
it will return data if it exists in the buffer. Otherwise, it
will block the caller to the read() method until the
data requested is received, the pipe is closed, or the pipe is
broken.
| NOTE |
A pipe is determined to be broken when the thread of either end of a pipe terminates |
The PipedOutputStream has an easier job. As described above, once connected to a PipedInputStream object, it simply calls the input pipe's receive() method to send data down the pipe. One point to keep in mind is that calls to the write() methods will block if the input pipe does not have enough room in its internal buffer to hold the data being sent. However, if you run your pipe I/O in separate threads, blocking will not affect the responsiveness of your program.
The following example illustrates the use of pipes to communicate between two applets running on the same Web page. One applet sits in a loop spinning an animated globe. It also creates a thread that is responsible for monitoring an input pipe for commands from another applet that change the manner in which the globe is animated. The second applet displays six pushbuttons that enable the user to start and stop the animation, change the rotation of the globe, and increase or decrease the speed of the animation. When one of the buttons is pressed, a command is sent to the animation applet through an output pipe. Figure 13.1 displays the Web page and the two applets in action.
Figure 13.1 : The pipe stream example.
Listing 13.7 includes a portion of the source code for the first applet and the thread class used to monitor the input pipe. The basis for the animation in the applet may look familiar to you because it is the default animation created by the Visual J++ Applet Wizard. A few enhancements were made to integrate the pipe monitor thread and the custom animation. The complete source listing can be found on the accompanying CD-ROM.
Listing 13.7. EX13G.java.
import java.applet.*;
import java.awt.*;
import java.io.*;
public class EX13G extends Applet implements Runnable
{
//...
private PipedInputStream inputPipe;
private PipeMonitor monitor;
public EX13G()
{ // Create input pipe and share with pipe monitor.
inputPipe = new PipedInputStream();
monitor = new PipeMonitor(inputPipe);
}
//...
public void run()
{
if (!m_fAllLoaded)
{ // Load all images first.
if (!loadImages())
return;
}
while (true)
{ // Check to see if globe should be animated.
if (monitor.isAnimated())
{
try
{ // Display image and move on to next image.
displayImage(m_Graphics);
if (monitor.getRotation() == monitor.FORWARD)
{ // Move forward through image array.
if (++m_nCurrImage == NUM_IMAGES)
m_nCurrImage = 0;
}
else
{ // Move backwards through image array.
if (--m_nCurrImage < 0)
m_nCurrImage = NUM_IMAGES - 1;
}
// Rate of animation is controlled by sleep time.
Thread.currentThread().sleep(monitor.getSleepTime());
}
catch (InterruptedException e)
{
stop();
monitor.stop();
}
}
}
}
//...
public void connectPipes(PipedOutputStream out) throws IOException
{ // Called by controller applet to connect input & output pipes.
inputPipe.connect(out);
// Only start pipe monitor if necessary.
if (!monitor.isAlive())
monitor.start(); // Waits for commands sent through pipe.
}
}
class PipeMonitor extends Thread
{
public final int FORWARD = 1;
public final int REVERSE = 2;
// Setup attributes with default values.
private int rotation = FORWARD;
private int sleepTime = 50;
private boolean animated = true;
private DataInputStream inputPipe;
private PipeMonitor() {} // Hide default ctor.
public PipeMonitor(PipedInputStream pi)
{
inputPipe = new DataInputStream(pi);
}
// Access methods.
public synchronized int getRotation() { return rotation; }
public synchronized int getSleepTime() { return sleepTime; }
public synchronized boolean isAnimated() { return animated; }
public void run()
{
String cmd;
while (true)
{
try
{ // Wait for a ('\n' terminated) command from the pipe.
do
cmd = inputPipe.readLine();
while (cmd == null);
// Interpret command and change settings.
if (cmd.equalsIgnoreCase("FORWARD"))
rotation = FORWARD;
else if (cmd.equalsIgnoreCase("REVERSE"))
rotation = REVERSE;
else if (cmd.equalsIgnoreCase("START"))
animated = true;
else if (cmd.equalsIgnoreCase("STOP"))
animated = false;
else if (cmd.equalsIgnoreCase("FASTER"))
sleepTime -= (sleepTime == 10 ? 0 : 10);
else if (cmd.equalsIgnoreCase("SLOWER"))
sleepTime += 10;
}
catch (IOException e) {}
}
}
}
The source code for the second applet is shown in Listing 13.8. The command() method is where the connection is made with the animation applet and the commands are sent down the output pipe. A DataOutputStream filter is attached to the pipe to allow strings to be sent across the connection.
Listing 13.8. EX13G2.java.
import java.applet.*;
import java.awt.*;
import java.io.*;
import EX13G;
public class EX13G2 extends Applet
{
private DataOutputStream outputPipe = null;
public void init()
{ // Add control buttons to manipulate globe.
add(new Button("Start"));
add(new Button("Stop"));
add(new Button("Forward"));
add(new Button("Reverse"));
add(new Button("Faster"));
add(new Button("Slower"));
}
public boolean action(Event event, Object obj)
{
boolean retval = false;
if (event.target instanceof Button)
{
try
{ // Send the appropriate command based
// on the label of the button pressed.
command(((Button)event.target).getLabel());
}
catch (IOException e)
{
// Add your exception logic here.
}
retval = true;
}
return retval;
}
public void command(String cmd) throws IOException
{
if (outputPipe == null)
{ // Pipe needs to be created and connected to
// the animated globe applet.
// Get a reference to the animated globe applet.
EX13G ex13g;
ex13g = (EX13G)getAppletContext().getApplet("globe");
if (ex13g == null)
{
throw new IOException("globe applet could not be found.");
}
// Setup output pipe.
PipedOutputStream p = new PipedOutputStream();
ex13g.connectPipes(p);
outputPipe = new DataOutputStream(p);
}
outputPipe.writeBytes(cmd + "\n");
}
}
The HTML used to run the two applets is shown in Listing 13.9. It is critical that the name of the first applet be set correctly so that the command applet can make the connection.
Listing 13.9. EX13G.html.
<HTML> <HEAD> <TITLE>Example 13g - Pipes</TITLE> </HEAD> <BODY> <H2>Example 13g - Pipes</H2> <HR> Applet 1 - Spinning Globe<BR> <APPLET CODE=EX13G.class WIDTH=300 HEIGHT=100 NAME=globe></APPLET> <HR> Applet 2 - Globe Controller<BR> <APPLET CODE=EX13G2.class WIDTH=300 HEIGHT=30 NAME=controller></APPLET> <HR> </BODY> </HTML>
Java's stream classes provide a flexible and powerful framework for transferring data between objects. Receivers and senders of data across a stream can range from in-memory byte arrays to files on a disk drive. And as we will see in Part IV, streams serve as the foundation for Java's impressive and easy-to-use networking features.