In this chapter you'll learn to use Java streams to perform sophisticated input and output using standard I/O, memory buffers, and files. You'll be introduced to all classes of the java.io package. You'll explore the input and output stream class hierarchy and learn to use stream filters to simplify I/O processing. You'll also learn how to perform random-access I/O and how to use the StreamTokenizer class to construct input parsers. When you finish this chapter, you'll be able to add sophisticated I/O processing to your Java programs.
Java input and output is based on the use of streams. Streams are sequences of bytes that travel from a source to a destination over a communication path. If your program is writing to a stream, it is the stream's source. If it is reading from a stream, it is the stream's destination. The communication path is dependent on the type of I/O being performed. It can consist of memory-to-memory transfers, file system, network, and other forms of I/O.
Streams are not complicated. They are powerful because they abstract away the details of the communication path from input and output operations. This allows all I/O to be performed using a common set of methods. These methods can be tailored and extended to provide higher-level custom I/O capabilities.
Java defines two major classes of streams: InputStream and OutputStream. These streams are subclassed to provide a variety of I/O capabilities.
Figure 13.1 identifies the java.io class hierarchy. As described in the previous section, the InputStream and OutputStream classes are the major components of this hierarchy. Other high-level classes include the File, FileDescriptor, RandomAccessFile, and StreamTokenizer classes.
Figure 13.1 :The classes of the java.io hierarchy.
The InputStream and OutputStream classes have complementary subclasses. For example, both have subclasses for performing I/O via memory buffers, files, and pipes. The InputStream subclasses perform the input and the OutputStream classes perform the output.
The InputStream class has six subclasses. The ByteArrayInputStream class is used to convert an array into an input stream. The StreamBufferInputStream class uses a StreamBuffer as an input stream. The FileInputStream allows files to be used as input streams. The PipedInputStream class allows a pipe to be constructed between two threads and supports input through the pipe. The SequenceInputStream class allows two or more streams to be concatenated into a single stream. The FilterInputStream class is an abstract class from which other input-filtering classes are constructed.
Filters are objects that read from one stream and write to another, usually altering the data in some way as they pass it from one stream to another. Filters can be used to buffer data, read and write objects, keep track of line numbers, and perform other operations on the data they move. Filters can be combined, with one filter using the output of another as its input. You can create custom filters by combining existing filters.
FilterInputStream has four filtering subclasses. The BufferedInputStream class maintains a buffer of the input data that it receives. This eliminates the need to read from the stream's source every time an input byte is needed. The DataInputStream class implements the DataInput interface, a set of methods that allow objects and primitive data types to be read from a stream. The LineNumberInputStream class is used to keep track of input line numbers. The PushbackInputStream class provides the capability to push data back onto the stream that it is read from so that it can be read again.
The OutputStream class hierarchy consists of four major subclasses. The ByteArrayOutputStream, FileOutputStream, and PipedOutputStream classes are the output complements to the ByteArrayInputStream, FileInputStream, and PipedInputStream classes. The FilterOutputStream class provides subclasses that complement the FilterInputStream classes.
The BufferedOutputStream class is the output analog to the BufferedInputStream class. It buffers output so that output bytes can be written to devices in larger groups. The DataOutputStream class implements the DataOutput interface. This interface complements the DataInput interface. It provides methods that write objects and primitive data types to streams so that they can be read by the DataInput interface methods. The PrintStream class provides the familiar print() and println() methods used in most of the sample programs that you've developed so far in this book. It provides a number of overloaded methods that simplify data output.
The File class is used to access the files and directories of the local file system. The FileDescriptor class is an encapsulation of the information used by the host system to track files that are being accessed. The RandomAccessFile class provides the capabilities needed to directly access data contained in a file. The StreamTokenizer class is used to create parsers that operate on stream data.
The InputStream class is an abstract class that lays the foundation for the Java Input class hierarchy. As such, it provides methods that are inherited by all InputStream classes.
The read() method is the most important method of the InputStream class hierarchy. It reads a byte of data from an input stream and blocks if no data is available. When a method blocks, it causes the thread in which it is executing to wait until data becomes available. This is not a problem in multithreaded programs. The read() method takes on several overloaded forms. It can read a single byte or an array of bytes, depending upon what form is used. It returns the number of bytes read or -1 if an end of file is encountered with no bytes read.
The read() method is overridden and overloaded by subclasses to provide custom read capabilities.
The available() method returns the number of bytes that are available to be read without blocking. It is used to peek into the input stream to see how much data is available. However, depending on the input stream, it might not be accurate or useful. Some input streams on some operating systems may always report 0 available bytes. In general, it is not a good idea to blindly rely on this method to perform input processing.
The close() method closes an input stream and releases resources associated with the stream. It is always a good idea to close a stream to ensure that the stream processing is correctly terminated.
Java supports markable streams. These are streams that provide the capability to mark a position in the stream and then later reset the stream so that it can be reread from the marked position. If a stream can be marked, it must contain some memory associated with it to keep track of the data between the mark and the current position of the stream. When this buffering capability is exceeded, the mark becomes invalid.
The markSupported() method returns a boolean value that identifies whether a stream supports mark and reset capabilities. The mark() method marks a position in the stream. It takes an integer parameter that identifies the number of bytes that can be read before the mark becomes invalid. This is used to set the buffering capacity of the stream. The reset() method simply repositions the stream to its last marked position.
The skip() method skips over a specified number of input bytes. It takes a long value as a parameter.
The OutputStream class is an abstract class that lays the foundation for the output stream hierarchy. It provides a set of methods that are the output analog to the InputStream methods.
The write() method allows bytes to be written to the
output stream. It provides three overloaded forms to write a single
byte, an array of bytes, or a segment of an array. The write()
method, like the read() method, may block when it tries
to write to a stream. The blocking causes the thread executing
the write() method to wait until the write operation
has been completed.
Note |
The OutputStream class defines three overloaded forms for the write() method. These forms allow you to write an integer, an array of bytes, or a subarray of bytes to an OutputStream object. You will often see several overloaded forms for methods that perform the same operation using different types of data. |
The flush() method causes any buffered data to be immediately written to the output stream. Some subclasses of OutputStream support buffering and override this method to "clean out" their buffers and write all buffered data to the output stream. They must override the OutputStream flush() method because, by default, it does not perform any operations and is used as a placeholder.
It is generally more important to close output streams than input streams, so that any data written to the stream is stored before the stream is deallocated and lost. The close() method of OutputStream is used in the same manner as that of InputStream.
Java supports byte array input and output via the ByteArrayInputStream and ByteArrayOutputStream classes. These classes use memory buffers as the source and destination of the input and output streams. These streams do not have to be used together. They are covered in the same section here because they provide similar and complementary methods. The StringBufferInputStream class is similar to the ByteArrayInput class and is also covered in this section.
The ByteArrayInputStream class creates an input stream from a memory buffer. The buffer is an array of bytes. It provides two constructors that use a byte array argument to create the input stream. The class does not support any new methods, but overrides the read(), skip(), available(), and reset() methods of InputStream.
The read() and skip() methods are implemented as specified for InputStream. The method available() is reliable and can be used to check on the number of available bytes in the buffer. The reset() method does not work with a mark() method; it simply resets to the beginning of the buffer.
The ByteArrayOutputStream class is a little more sophisticated than its input complement. It creates an output stream on a byte array, but provides additional capabilities to allow the output array to grow to accommodate new data that is written to it. It also provides the toByteArray() and toString() methods for converting the stream to a byte array or String object.
ByteArrayOutputStream provides two constructors. One takes an integer argument that is used to set the output byte array to an initial size. The other constructor does not take an argument and sets the output buffer to a default size.
ByteArrayOutputStream provides some additional methods not declared for OutputStream. The reset() method resets the output buffer to allow writing to restart at the beginning of the buffer. The size() method returns the current number of bytes that have been written to the buffer. The writeTo() method is new. It takes an object of class OutputStream as an argument and writes the contents of the output buffer to the specified output stream. The write() methods override those of OutputStream to support array output.
Having learned about both sides of the byte array I/O classes, you now have a base from which to create a sample program. Remember to create a ch13 directory under \java\jdg in which to store the files created in this chapter. The source code of the ByteArrayIOApp program is provided in Listing 13.1.
Listing 13.1. The source code of the ByteArrayIOApp program.import java.lang.System;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ByteArrayIOApp {
public static void main(String args[]) throws IOException {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
System.out.println("outstream: "+outStream);
System.out.println("size: "+outStream.size());
ByteArrayInputStream inStream;
inStream = new ByteArrayInputStream(outStream.toByteArray());
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
}
}
The program creates a ByteArrayOutputStream object, outStream, and an array, s, that contains the text "This is a test." to be written to the stream. Each character of s is written, one at a time, to outStream. The contents of outstream are then printed, along with the number of bytes written.
A ByteArrayInputStream object, inStream, is created by invoking the toByteArray() method of outStream to create a byte array that is used as an argument to the ByteArrayInputStream constructor. The available() method is used to determine the number of available input bytes stored in the buffer. This number is stored as inBytes and is used to allocate a byte array to store the data that is read. The read() method is invoked for inStream to read inBytes worth of data. The actual number of bytes read is stored in bytesRead. This number is displayed, followed on the next line by the bytes that were read from inStream, as follows:
outstream: This is a test.
size: 15
inStream has 15 available bytes
15 bytes were read
They are: This is a test.
StringBufferInputStream is similar to ByteArrayInputStream except that it uses a StringBuffer to store input data. The input stream is constructed using a String argument. Its methods are identical to those provided by ByteArrayInputStream.
Java supports stream-based file input and output through the File, FileDescriptor, FileInputStream, and FileOutputStream classes. It supports direct- or random-access I/O using the File, FileDescriptor, and RandomAccessFile classes. Random-access I/O is covered later in this chapter.
The File class provides access to file and directory objects and supports a number of operations on files and directories. The FileDescriptor class encapsulates the information used by the host system to track files that are being accessed. The FileInputStream and FileOutputStream classes provide the capability to read and write to file streams.
The File class is used to access file and directory objects. It uses the file-naming conventions of the host operating system. The File class encapsulates these conventions using the File class constants.
File provides constructors for creating files and directories. These constructors take absolute and relative file paths and file and directory names.
The File class provides numerous access methods that can be used to perform all common file and directory operations. It is important for you to review the API page for this class because file I/O and file and directory operations are common to most programs.
File methods allow files to be created, deleted, and renamed. They provide access to a file's path and name and determine whether a File object is a file or directory. These methods also check read and write access permissions.
Directory methods allow directories to be created, deleted, renamed, and listed. Directory methods also allow directory trees to be traversed by providing access to the parent and sibling directories.
The FileDescriptor class provides access to the file descriptors maintained by operating systems when files and directories are being accessed. This class is opaque in that it does not provide visibility into the specific information maintained by the operating system. It provides only one method, the valid() method, which is used to determine whether a file descriptor object is currently valid.
The FileInputStream class allows input to be read from a file in the form of a stream. Objects of class FileInputStream are created using a filename string or a File or FileDescriptor object as an argument. FileInputStream overrides the methods of the InputStream class and provides two new methods, finalize() and getFD(). The finalize() method is used to close a stream when it is processed by the Java garbage collector. The getFD() method is used to obtain access to the FileDescriptor associated with the input stream.
The FileOutputStream class allows output to be written to a file stream. Objects of class FileOutputStream are created in the same way as those of class FileInputStream, using a file- name string, File object, or FileDescriptor object as an argument. FileOutputStream overrides the methods of the OutputStream class and supports the finalize() and getFD() methods described for the FileInputStream class.
The program in Listing 13.2 illustrates the use of the FileInputStream, FileOutputStream, and File classes. It writes a string to an output file and then reads the file to verify that the output was written correctly. The file used for the I/O is then deleted.
Listing 13.2. The source code of the FileIOApp program.import java.lang.System;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
public class FileIOApp {
public static void main(String args[]) throws IOException {
FileOutputStream outStream = new FileOutputStream("test.txt");
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
outStream.close();
FileInputStream inStream = new FileInputStream("test.txt");
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
inStream.close();
File f = new File("test.txt");
f.delete();
}
}
The FileOutputStream constructor creates an output stream on the file test.txt. The file is automatically created in the current working directory. It then writes the string "This is a test." to the output file stream. Note the similarity between this program and the previous one. The power of streams is that the same methods can be used no matter what type of stream is being used.
The output stream is closed to make sure that all the data is written to the file. The file is then reopened as an input file by creating an object of class FileInputStream. The same methods used in the ByteArrayIOApp program are used to determine the number of available bytes in the file and read these bytes into a byte array. The number of bytes read is displayed along with the characters corresponding to those bytes.
The input stream is closed and then a File object is created to provide access to the file. The File object is used to delete the file using the delete() method. The program's output follows:
inStream has 15 available bytes
15 bytes were read
They are: This is a test.
The SequenceInputStream class is used to combine two or more input streams into a single input stream. The input streams are concatenated, which allows the individual streams to be treated as a single, logical stream. The SequenceInputStream class does not introduce any new access methods. Its power is derived from the two constructors that it provides. One constructor takes two InputStream objects as arguments. The other takes an Enumeration of InputStream objects. The Enumeration interface is described in Chapter 14, "Useful Tools in the java.util Package." It provides methods for dealing with a sequence of related objects.
The program in Listing 13.3 reads the two Java source files, ByteArrayIOApp.java and FileIOApp.java, as a single file courtesy of the SequenceInputStream class.
Listing 13.3. The source code of the SequenceIOApp program.import java.lang.System;
import java.io.FileInputStream;
import java.io.SequenceInputStream;
import java.io.IOException;
public class SequenceIOApp {
public static void main(String args[]) throws IOException {
SequenceInputStream inStream;
FileInputStream f1 = new FileInputStream("ByteArrayIOApp.java");
FileInputStream f2 = new FileInputStream("FileIOApp.java");
inStream = new SequenceInputStream(f1,f2);
boolean eof = false;
int byteCount = 0;
while (!eof) {
int c = inStream.read();
if(c == -1) eof = true;
else{
System.out.print((char) c);
++byteCount;
}
}
System.out.println(byteCount+" bytes were read");
inStream.close();
f1.close();
f2.close();
}
}
The program creates two objects of class FileInputStream for the files ByteArrayIOApp.java and FileIOApp.java. The SequenceInputClass constructor is used to construct a single input stream from the two FileInputStream objects. The program then uses a while loop to read all bytes in the combined file and display them to the console window. The loop stops when the end of the combined file is encountered. This is signaled when the read() method returns -1. The streams are closed after the combined files have been read. The program's output is as follows:
import java.lang.System;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ByteArrayIOApp {
public static void main(String args[]) throws IOException {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
System.out.println("outstream: "+outStream);
System.out.println("size: "+outStream.size());
ByteArrayInputStream inStream;
inStream = new ByteArrayInputStream(outStream.toByteArray());
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
}
}
import java.lang.System;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
public class FileIOApp {
public static void main(String args[]) throws IOException {
FileOutputStream outStream = new FileOutputStream("test.txt");
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
outStream.close();
FileInputStream inStream = new FileInputStream("test.txt");
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
inStream.close();
File f = new File("test.txt");
f.delete();
}
}
1771 bytes were read
The SequenceIOApp program displays the combined contents of the two source files followed by a line identifying the number of bytes that were read.
The filtered input and output stream classes provide the capability to filter I/O in a number of useful ways. I/O filters are used to adapt streams to specific program needs. These filters sit between an input stream and an output stream and perform special processing on the bytes they transfer from input to output. You can combine filters to perform a sequence of filtering operations where one filter acts on the output of another, as shown in Figure 13.2.
Figure 13.2 :Combining filters.
The FilterInputStream class is an abstract class that is the parent of all filtered input stream classes. The FilterInputStream class provides the basic capability to create one stream from another. It allows one stream to be read and provided as output as another stream. This is accomplished through the use of the in variable, which is used to maintain a separate object of class InputStream. The design of the FilterInputStream class allows multiple chained filters to be created using several layers of nesting. Each subsequent class accesses the output of the previous class through the in variable. Because the in variable is an object of class InputStream, arbitrary InputStream objects can be filtered.
The FilterOutputStream class is the complement of the FilterInputStream class. It is an abstract class that is the parent of all filtered output stream classes. It is similar to the FilterInputStream class in that it maintains an object of class OutputStream as an out variable. Data written to an object of FilterOutputStream can be modified as needed to perform filtering operations and then forwarded to the out OutputStream object. Because out is declared to be of class OutputStream, arbitrary output streams can be filtered. Multiple FilterOutputStream objects can be combined in a manner that is analogous to FilterInputStream objects. The input of subsequent FilterOutputStream objects is linked to the output of preceding objects.
Buffered input and output is used to temporarily cache data that is read from or written to a stream. This allows programs to read and write small amounts of data without adversely affecting system performance. When buffered input is performed, a large number of bytes are read at a single time and stored in an input buffer. When a program reads from the input stream, the input bytes are read from the input buffer. Several reads may be performed before the buffer needs to refilled. Input buffering is used to speed up overall stream input processing.
Output buffering is performed in a manner similar to input buffering. When a program writes to a stream, the output data is stored in an output buffer until the buffer becomes full or the output stream is flushed. Only then is the buffered output actually forwarded to the output stream's destination.
Java implements buffered I/O as filters. The filters maintain and operate the buffer that sits between the program and the source or destination of a buffered stream.
The BufferedInputStream class supports input buffering by automatically creating and maintaining a buffer for a designated input stream. This allows programs to read data from the stream one byte at a time without degrading system performance. Because the BufferedInputStream class is a filter, it can be applied to arbitrary objects of class InputStream and combined with other input filters.
The BufferedInputStream class uses several variables to implement input buffering. These variables are described in the Java API page for this class. However, because these variables are declared as protected, they cannot be directly accessed by your program.
BufferedInputStream defines two constructors. One allows the size of an input buffer to be specified and the other does not. Both constructors take an object of class InputStream as an argument. It is usually better to let BufferedInputStream select the best size for the input buffer than to specify one yourself unless you have specific knowledge that one buffer size is better than another.
BufferedInputStream overrides the access methods provided by InputStream and does not introduce any new methods of its own.
The BufferedOutputStream class performs output buffering in a manner that is analogous to BufferedInputStream. It allows the size of the output buffer to be specified in a constructor as well as providing for a default buffer size. It overrides the methods of the OutputStream class and does not introduce any new methods of its own.
The BufferedIOApp program (see Listing 13.4) builds on the SequenceIOApp example that was presented previously. It performs buffering on the SequenceInputStream object used to combine the input from two separate files. It also performs buffering on program output so that characters do not need to be displayed to the console window a single character at a time.
Listing 13.4. The source code of the BufferedIOApp program.import java.lang.System;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.PrintStream;
import java.io.FileInputStream;
import java.io.SequenceInputStream;
import java.io.IOException;
public class BufferedIOApp {
public static void main(String args[]) throws IOException {
SequenceInputStream f3;
FileInputStream f1 = new FileInputStream("ByteArrayIOApp.java");
FileInputStream f2 = new FileInputStream("FileIOApp.java");
f3 = new SequenceInputStream(f1,f2);
BufferedInputStream inStream = new BufferedInputStream(f3);
BufferedOutputStream bufStream = new BufferedOutputStream(System.out);
PrintStream outStream = new PrintStream(bufStream);
inStream.skip(500);
boolean eof = false;
int byteCount = 0;
while (!eof) {
int c = inStream.read();
if(c == -1) eof = true;
else{
outStream.print((char) c);
++byteCount;
}
}
outStream.println(byteCount+" bytes were read");
inStream.close();
outStream.close();
f1.close();
f2.close();
}
}
The program begins by creating two objects of FileInputStream and combining them into a single input stream using the SequenceInputStream constructor. It then uses this stream to create an object of BufferedInputStream using the default buffer size.
A BufferedOutputStream object is created using the System.out output stream and a default buffer size. Another filter is applied using the PrintStream class. PrintStream is an output-filtering subclass of FilterOutputStream. It provides several overloaded versions of the print() and println() methods for facilitating program output.
The skip() method is used to skip over 500 bytes of the input stream. This is done for two reasons: to illustrate the use of the skip() method and to cut down on the size of the program output. The rest of the input is read and printed as in the previous example.
The program output is similar to that of the preceding example. The skip() method was used to skip over 500 bytes of input. These bytes are also absent from the program's output. The program's output is as follows:
rrayInputStream inStream;
inStream = new ByteArrayInputStream(outStream.toByteArray());
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
}
}
import java.lang.System;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
public class FileIOApp {
public static void main(String args[]) throws IOException {
FileOutputStream outStream = new FileOutputStream("test.txt");
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
outStream.close();
FileInputStream inStream = new FileInputStream("test.txt");
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf,0,inBytes);
System.out.println(bytesRead+" bytes were read");
System.out.println("They are: "+new String(inBuf,0));
inStream.close();
File f = new File("test.txt");
f.delete();
}
}
1271 bytes were read
PushbackInputStream is a filter that lets you push a byte that was previously read back onto the input stream so that it can be reread. This type of filter is commonly used with parsers. When a character indicating a new input token is read, it is pushed back onto the input stream until the current input token is processed. It is then reread when processing of the next input token is initiated. PushbackInputStream allows only a single byte to be pushed back. This is generally enough for most applications.
The pushback character is stored in a variable named pushBack.
The unread() method is the only new method introduced by this class. It is used to push a specified character back onto the input stream.
The PushbackIOApp program illustrates the use of the PushbackInputStream class. (See Listing 13.5.) It adds a pushback filter to the ByteArrayIOApp program studied earlier in this chapter.
Listing 13.5. The source code of the PushbackIOApp program.import java.lang.System;
import java.io.PushbackInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class PushbackIOApp {
public static void main(String args[]) throws IOException {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
String s = "This is a test.";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
System.out.println("outstream: "+outStream);
System.out.println("size: "+outStream.size());
ByteArrayInputStream inByteArray;
inByteArray = new ByteArrayInputStream(outStream.toByteArray());
PushbackInputStream inStream;
inStream = new PushbackInputStream(inByteArray);
char ch = (char) inStream.read();
System.out.println("First character of inStream is "+ch);
inStream.unread((int) 't');
int inBytes = inStream.available();
System.out.println("inStream has "+inBytes+" available bytes");
byte inBuf[] = new byte[inBytes];
for(int i=0;i<inBytes;++i) inBuf[i]=(byte) inStream.read();
System.out.println("They are: "+new String(inBuf,0));
}
}
PushbackIOApp creates a stream to be used for byte array input using the code of the ByteArrayIOApp program. It applies a pushback filter to this stream by using the PushbackInputStream filter to create an object of class PushbackInputStream. It reads the first character of the input stream and displays it. It then pushes back a t onto the input stream. Note that any character could have been pushed back upon the input stream. The new input stream is then read and displayed.
The program output shows how the pushback filter was used to change the first character of the input stream from an uppercase T to a lowercase t. The program output consists of the following:
outstream: This is a test.
size: 15
First character of inStream is T
inStream has 15 available bytes
They are: this is a test.
The LineNumberInputStream class provides a handy capability for keeping track of input line numbers. It is also a subclass of FilterInputStream. This class provides two new methods to support line-number processing. The setLineNumber() method is used to set the current line number to a particular value. The getLineNumber() method is used to obtain the value of the current line number.
The LineNumIOApp program illustrates the use of this filter class. (See Listing 13.6.)
Listing 13.6. The source code of the LineNumIOApp program.import java.lang.System;
import java.io.LineNumberInputStream;
import java.io.FileInputStream;
import java.io.DataInputStream;
import java.io.IOException;
public class LineNumIOApp {
public static void main(String args[]) throws IOException {
FileInputStream inFile = new FileInputStream("LineNumIOApp.java");
LineNumberInputStream inLines = new LineNumberInputStream(inFile);
DataInputStream inStream = new DataInputStream(inLines);
String inputLine;
while ((inputLine=inStream.readLine()) != null) {
System.out.println(inLines.getLineNumber()+". "+inputLine);
}
}
}
LineNumIOApp reads the LineNumIOApp.java source file and displays it using line numbers. It uses three nested input stream objects. First it creates a FileInputStream object and assigns it to the inFile variable. It then uses this object to create a LineNumberInputStream object, which it assigns to inLines. Finally, it creates a DataInputStream object using inLines and assigns it to inStream. The DataInputStream class is described in the "Data I/O" section of this chapter.
A while loop is used to read every line of the program's source file and display it along with its line number. The readline() method indicates an end-of-file condition by returning a null value. Otherwise, it returns a string with the value of the last line that was read. Notice that the getLineNumber() method was applied to inLines and not to inStream. This is because inStream is an object of DataInputStream and does not support this method.
The program's output provides a nice example of the capabilities of the LineNumberInputStream:
1.
2. import java.lang.System;
3. import java.io.LineNumberInputStream;
4. import java.io.FileInputStream;
5. import java.io.DataInputStream;
6. import java.io.IOException;
7.
8. public class LineNumIOApp {
9. public static void main(String args[]) throws IOException {
10. FileInputStream inFile = new FileInputStream("LineNumIOApp.java");
11. LineNumberInputStream inLines = new LineNumberInputStream(inFile);
12. DataInputStream inStream = new DataInputStream(inLines);
13. String inputLine;
14. while ((inputLine=inStream.readLine()) != null) {
15. System.out.println(inLines.getLineNumber()+". "+inputLine);
16. }
17. }
18. }
The DataInputStream and DataOutputStream classes implement the DataInput and DataOutput interfaces. These interfaces identify methods that provide the capability to allow arbitrary objects and primitive data types to be read and written from a stream. By implementing these interfaces, the DataInputStream and DataOutputStream classes provide the basis for the implementation of portable input and output streams.
The DataInputStream class provides the capability to read arbitrary objects and primitive types from an input stream. As you saw in the previous programming example, the filter provided by this class can be nested with other input filters.
This class implements the methods of the DataInput interface. These methods provide a full range of input capabilities. You should check out the Java API pages for the DataInputStream class to familiarize yourself with these methods.
Note that most, but not all, of these methods raise the EOFException when an end of file is encountered. The readLine() method returns a null value to signify a read past the end of a file.
The DataOutputStream class provides an output complement to DataInputStream. It allows arbitrary objects and primitive data types to be written to an output stream. It also keeps track of the number of bytes written to the output stream. It is an output filter and can be combined with any output-filtering streams.
The program in Listing 13.7 shows how DataInputStream and DataOutputStream can be used to easily read and write a variety of values using streams.
Listing 13.7. The source code of the DataIOApp program.import java.lang.System;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
public class DataIOApp {
public static void main(String args[]) throws IOException {
File file = new File("test.txt");
FileOutputStream outFile = new FileOutputStream(file);
DataOutputStream outStream = new DataOutputStream(outFile);
outStream.writeBoolean(true);
outStream.writeInt(123456);
outStream.writeChar('j');
outStream.writeDouble(1234.56);
System.out.println(outStream.size()+" bytes were written");
outStream.close();
outFile.close();
FileInputStream inFile = new FileInputStream(file);
DataInputStream inStream = new DataInputStream(inFile);
System.out.println(inStream.readBoolean());
System.out.println(inStream.readInt());
System.out.println(inStream.readChar());
System.out.println(inStream.readDouble());
inStream.close();
inFile.close();
file.delete();
}
}
The program creates an object of class File that is used to access the test.txt file. This object is used to create an instance of class FileOutputStream that is assigned to the outFile variable. An object of class DataOutputStream is then constructed as a filter for the FileOutputStream object.
The writeBoolean(), writeChar(), writeInt(), and writeDouble() methods of DataOutputStream are used to write examples of primitive data types to the filtered output stream. The number of bytes written to the output stream is determined from the size() method and displayed to the console window. The output streams are then closed.
The File object, created at the beginning of the program, is then used to create an object of class FileInputStream. The output stream is then filtered by creating an object of DataInputStream.
The primitive data types that were written to the output file in the beginning of the program are now read from the filtered input stream and displayed to the console window.
The program's output shows that the data values were successfully written and read using the data I/O filters:
15 bytes were written
true
123456
j
1234.56
The PrintStream class should be no stranger to you. The System.out object that you have been using for most of the example programs is an instance of the PrintStream class. It is used to write output to the Java console window.
PrintStream's power lies in the fact that it provides two methods, print() and println(), that are overloaded to print any primitive data type or object. Objects are printed by first converting them to strings using their toString() method inherited from the Object class. To provide custom printing for any class, all you have to do is override the toString() method for that class.
PrintStream provides the capability to automatically flush all output bytes in the stream when a newline character is written to the stream. This feature can be enabled or disabled when the stream is created.
Because PrintStream is a filter, it takes an instance of OutputStream as an argument to its constructor. A second constructor adds the capability to use the autoflushing feature.
PrintStream introduces only one new method besides the extensively overloaded print() and println() methods. The checkError() method is used to flush stream output and determine whether an error occurred on the output stream. This capability is useful for printing output to devices, such as printers, where error status is needed to notify the user of any changes to the device state.
Piped I/O provides the capability for threads to communicate via streams. A thread sends data to another thread by creating an object of PipedOutputStream that it connects to an object of PipedInputStream. The output data written by one thread is read by another thread using the PipedInputStream object.
The process of connecting piped input and output threads is symmetric. An object of class PipedInputThread can also be connected to an existing object of class PipedOutputThread.
Java automatically performs synchronization with respect to piped input and output streams. The thread that reads from an input pipe does not have to worry about any conflicts with tasks that are writing to the corresponding output stream thread.
Both PipedInputStream and PipedOutputStream override the standard I/O methods of InputStream and OutputStream. The only new method provided by these classes is the connect() method. Both classes provide the capability to connect a piped stream when it is constructed by passing the argument of the piped stream to which it is to be connected as an argument to the constructor.
The PipedIOApp program creates two threads of execution, named Producer and Consumer, that communicate using connected objects of classes PipedOutputStream and PipedInputStream. Producer sends the message This is a test. to Consumer one character at a time, and Consumer reads the message in the same manner. Producer displays its name and any characters that it writes to the console window. Consumer reads the message and displays its name and the characters it reads to the console window. The source code for the PipedIOApp program is shown in Listing 13.8.
Listing 13.8. The source code of the PipedIOApp program.import java.lang.Thread;
import java.lang.System;
import java.lang.InterruptedException;
import java.lang.Runnable;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.IOException;
class PipedIOApp {
public static void main(String args[]) {
Thread thread1 = new Thread(new PipeOutput("Producer"));
Thread thread2 = new Thread(new PipeInput("Consumer"));
thread1.start();
thread2.start();
boolean thread1IsAlive = true;
boolean thread2IsAlive = true;
do {
if(thread1IsAlive && !thread1.isAlive()){
thread1IsAlive = false;
System.out.println("Thread 1 is dead.");
}
if(thread2IsAlive && !thread2.isAlive()){
thread2IsAlive = false;
System.out.println("Thread 2 is dead.");
}
}while(thread1IsAlive || thread2IsAlive);
}
}
class PipeIO {
static PipedOutputStream outputPipe = new PipedOutputStream();
static PipedInputStream inputPipe = new PipedInputStream();
static {
try {
outputPipe.connect(inputPipe);
}catch (IOException ex) {
System.out.println("IOException in static initializer");
}
}
String name;
public PipeIO(String id) {
name = id;
}
}
class PipeOutput extends PipeIO implements Runnable {
public PipeOutput(String id) {
super(id);
}
public void run() {
String s = "This is a test.";
try {
for(int i=0;i<s.length();++i){
outputPipe.write(s.charAt(i));
System.out.println(name+" wrote "+s.charAt(i));
}
outputPipe.write('!');
} catch(IOException ex) {
System.out.println("IOException in PipeOutput");
}
}
}
class PipeInput extends PipeIO implements Runnable {
public PipeInput(String id) {
super(id);
}
public void run() {
boolean eof = false;
try {
while (!eof) {
int inChar = inputPipe.read();
if(inChar != -1) {
char ch = (char) inChar;
if(ch=='!'){
eof=true;
break;
}else System.out.println(name+" read "+ch);
}
}
} catch(IOException ex) {
System.out.println("IOException in PipeOutput");
}
}
}
This program is somewhat longer than the other examples in this chapter due to the overhead needed to set up the threading. The main() method creates the two Producer and Consumer threads as objects of classes PipeOutput and PipeInput. These classes are subclasses of PipeIO that implement the Runnable interface. The main() method starts both threads and then loops, checking for their death.
The PipeIO class is the superclass of the PipeOutput and PipeInput classes. It contains the static variables, outputPipe and inputPipe, that are used for interthread communication. These variables are assigned objects of classes PipedOutputStream and PipeInputStream. The static initializer is used to connect outputPipe with inputPipe using the connect() method. The PipeIO constructor provides the capability to maintain the name of its instances. This is used by the PipeInput and PipeOutput classes to store thread names.
The PipeOutput class extends PipeIO and implements the Runnable interface, making it eligible to be executed as a separate thread. The required run() method performs all thread processing. It loops to write the test message one character at a time to the outputPipe. It also displays its name and the characters that it writes to the console window. The ! character is used to signal the end of the message transmission. Notice that IOException is handled within the thread rather than being identified in the throws clause of the run() method. In order for run() to properly implement the Runnable interface, it cannot throw any exceptions.
The PipeInput class also extends PipeIO and implements the Runnable interface. It simply loops and reads one character at a time from inputPipe, displaying its name and the characters that it reads to the console window. It also handles IOException in order to avoid having to identify the exception in its throws clause.
The output of PipeIOApp shows the time sequencing of the thread input and output taking place using the connected pipe I/O streams. The output generated by running the program on your computer will probably differ because of differences in your computer's execution speed and I/O performance. The output generated when I ran the program is as follows:
Producer wrote T
Producer wrote h
Producer wrote i
Producer wrote s
Producer wrote
Consumer read T
Consumer read h
Consumer read i
Producer wrote i
Producer wrote s
Producer wrote
Consumer read s
Consumer read
Producer wrote a
Producer wrote
Producer wrote t
Consumer read i
Consumer read s
Consumer read
Producer wrote e
Producer wrote s
Consumer read a
Consumer read
Consumer read t
Producer wrote t
Producer wrote .
Thread 1 is dead.
Consumer read e
Consumer read s
Consumer read t
Consumer read .
Thread 2 is dead.
The RandomAccessFile class provides the capability to perform I/O directly to specific locations within a file. The name "random access" comes from the fact that data can be read from or written to random locations within a file rather than as a continuous stream of information. Random access is supported through the seek() method, which allows the pointer corresponding to the current file position to be set to arbitrary locations within the file.
RandomAccessFile implements both the DataInput and DataOutput interfaces. This provides the capability to perform I/O using all objects and primitive data types.
RandomAccessFile also supports basic file read/write permissions, allowing files to be accessed in read-only or read-write mode. A mode stream argument is passed to the RandomAccessFile constructor as r or rw, indicating read-only and read-write file access. The read-only access attribute may be used to prevent a file from being inadvertently modified.
RandomAccessFile introduces several new methods besides those inherited from Object and implemented from DataInput and DataOutput. These methods include seek(), getFilePointer(), and length(). The seek() method sets the file pointer to a particular location within the file. The getFilePointer() method returns the current location of the file pointer. The length() method returns the length of the file in bytes.
The RandomIOApp program provides a simple demonstration of the capabilities of random-access I/O. It writes a boolean, int, char, and double value to a file and then uses the seek() method to seek to offset location 1 within the file. This is the position after the first byte in the file. It then reads the int, char, and double values from the file and displays them to the console window. Next, it moves the file pointer to the beginning of the file and reads the boolean value that was first written to the file. This value is also written to the console window. The source code of the RandomIOApp program is shown in Listing 13.9.
Listing 13.9. The source code of the RandomIOApp program.import java.lang.System;
import java.io.RandomAccessFile;
import java.io.IOException;
public class RandomIOApp {
public static void main(String args[]) throws IOException {
RandomAccessFile file = new RandomAccessFile("test.txt","rw");
file.writeBoolean(true);
file.writeInt(123456);
file.writeChar('j');
file.writeDouble(1234.56);
file.seek(1);
System.out.println(file.readInt());
System.out.println(file.readChar());
System.out.println(file.readDouble());
file.seek(0);
System.out.println(file.readBoolean());
file.close();
}
}
Although the processing performed by RandomIOApp is quite simple, it illustrates how random I/O allows you to move the file pointer to various locations within a file to directly access values and objects contained within the file.
The program's output is as follows:
123456
j
1234.56
true
The StreamTokenizer class is used by parsers to convert an input stream into a stream of lexical tokens. It uses special methods to identify parser parameters such as ordinary, whitespace, quote, and comment characters. These methods also enable and disable number and end-of-line parsing.
Seven variables are defined for the StreamTokenizer class, four of which are constant class variables. The TT_EOF, TT_EOL, TT_NUMBER, and TT_WORD constants are used to identify the type of input token encountered when parsing the input stream. The ttype variable is set either to one of these constants or to a single character based on the kind of token that is read from the input stream. The TT_ constants are used to indicate a number, word, end of line, or end of file. When a word token is read, the actual word is stored in the sval variable and ttype is set to TT_WORD. When a number token is read, its value is stored in the nval variable and ttype is set to TT_NUMBER. When other special characters, such as @ or *, are read from the input stream, they are assigned directly to the ttype variable.
The StreamTokenizer constructor takes an InputStream object as an argument and generates a StreamTokenizer object. The StreamTokenizer access methods can be divided into two groups: parser parameter-definition methods and stream-processing methods.
The parser parameter-definition methods are used to control the operation of the parser. The commentChar(), slashSlashComments(), and slashStarComments() methods are used to define comments. Comments are ignored by the parser. The whitespaceChars(), wordChars(), quoteChar(), ordinaryChar(), and ordinaryChars() methods are used to set the parser's token-generation parameters. The parseNumbers() and eolIsSignificant() methods toggle number and end-of-line parsing. The lowerCaseMode() method controls whether input words are converted to lowercase, and the resetSyntax() method is used to reset the syntax table, causing all characters to be treated as special characters.
The stream-processing methods are used to read tokens from the input stream, push tokens back out onto the input stream, and return the current line number associated with the input stream. The nextToken() method is used to get the next token from the input stream. The pushBack() method pushes the current token back out onto the input stream. The lineno() method returns the current line number associated with the input stream.
The toString() method of class Object is overwritten to allow printing of the current token.
The StreamTokenApp program demonstrates the ease with which StreamTokenizer can be used to create a parser. This program reads input from the standard input stream, parses input tokens, and displays the token type and value to the console window. (See Listing 13.10.)
Listing 13.10. The source code of the StreamTokenApp program.import java.lang.System;
import java.io.StreamTokenizer;
import java.io.DataInputStream;
import java.io.IOException;
public class StreamTokenApp {
public static void main(String args[]) throws IOException {
DataInputStream inData = new DataInputStream(System.in);
StreamTokenizer inStream = new StreamTokenizer(inData);
inStream.commentChar('#');
inStream.eolIsSignificant(true);
inStream.whitespaceChars(0,32);
boolean eof = false;
do {
int token=inStream.nextToken();
switch(token){
case inStream.TT_EOF:
System.out.println("EOF encountered.");
eof = true;
break;
case inStream.TT_EOL:
System.out.println("EOL encountered.");
break;
case inStream.TT_WORD:
System.out.println("Word: "+inStream.sval);
break;
case inStream.TT_NUMBER:
System.out.println("Number: "+inStream.nval);
break;
default:
System.out.println((char) token+" encountered.");
if(token=='!') eof=true;
}
} while(!eof);
}
}
The program creates a new object of class DataInputStream using System.in as an argument. It then converts the DataInputStream object into a StreamTokenizer object and assigns it to the inStream variable. It sets the comment-line character to #, makes the end-of-line character a significant token, and identifies all ASCII characters with values between 0 and 32 as whitespace characters.
Having set up the parser, StreamTokenApp reads tokens from inStream until the end of file is encountered. It uses a switch statement to identify the type and value of each token read.
The following is an example of the output produced by StreamTokenizer. Try running it with different input lines:
This is a test.
Word: This
Word: is
Word: a
Word: test.
EOL encountered.
123 456
Number: 123
Number: 456
EOL encountered.
12.34 56.78
Number: 12.34
Number: 56.78
EOL encountered.
@ $ % ^
@ encountered.
$ encountered.
% encountered.
^ encountered.
EOL encountered.
#This is a comment.
EOL encountered.
This is #a comment.
Word: This
Word: is
EOL encountered.
!
! encountered.
In this chapter you have learned to work with Java input and output streams to perform input and output using standard I/O, memory buffers, and files. You have explored the input and output stream class hierarchy and learned to use stream filters to simplify I/O processing. You have also learned how to perform random-access I/O and how to use the StreamTokenizer class to construct an input parser. In Chapter 14 you will learn how to use the utility classes provided in the java.util package.