by Clayton Walnum
All computer programs must accept input and generate output. That is, after all, basically what a computer does. Obviously, every computer language must have a way of dealing with input and output. Otherwise, it would be impossible to write a program. Java features a rich set of classes that represent everything from a general input or output stream to a sophisticated random-access file. You now get a chance to experiment with these important classes.
All data used with a computer system flows from the input through the computer to the output. It's this idea of data flow that leads to the term streams. That is, a stream is really nothing more than a flow of data. There are input streams that direct data from the outside world (usually from the keyboard) to the computer, and output streams that direct data toward output devices, such as the computer screen or a file. Because streams are general in nature, a basic stream does not specifically define which devices the data flows from or to. Just like a wire carrying electricity that's being routed to a light bulb, TV, or dishwasher, a basic input or output stream can be directed to or from many different devices.
In Java, streams are represented by classes. The simplest of these classes represents basic input and output streams that provide general streaming abilities. Java derives from the basic classes other classes that are more specifically oriented toward a certain type of input or output. All of these classes can be found in the java.io package:
Obviously, there are way too many stream classes to be covered thoroughly in a single chapter. An entire book could be written on Java I/O alone. For that reason, this chapter covers the most useful of the stream classes, concentrating on basic input and output, as well as file handling and inter-thread communications. You begin with a brief introduction to the classes, after which sample programs demonstrate how the classes work.
As with any well-developed class hierarchy, the more specific Java stream classes like FileInputStream and ByteArrayOutputStream rely upon the general base classes InputStream and OutputStream for their basic functionality. Because InputStream and OutputStream are abstract classes, you cannot use them directly. However, because all of Java's stream classes have InputStream or OutputStream in their family tree, you should know what these classes have to offer.
The InputStream class represents the basic input stream. As such, it defines a set of methods that all input streams need. These methods are listed, without their parameters, in Table 18.1.
Method | Description |
read() | Reads data into the stream. |
skip() | Skips over bytes in the stream. |
available() | Returns the number of bytes immediately available in the stream. |
mark() | Marks a position in the stream. |
reset() | Returns to the marked position in the stream. |
markSupported() | Returns a boolean value indicating whether or not the stream supports marking and resetting. |
close() | Closes the stream. |
int read() int read(byte b[]) int read(byte b[], int off, int len)
The first version of read() simply reads single bytes as integers from the input stream, returning -1 if there is no data left to read. The second version reads multiple bytes into a byte array, returning the number of bytes actually read. The third version also reads data into a byte array, but enables you to specify an offset (off) in the array at which to start storing characters, as well as to indicate the maximum number of bytes to read (len).
The signatures for the remaining methods look like this:
long skip(long n) int available() void mark(int readlimit) void reset() boolean markSupported() void close()
The counterpart to InputStream is the OutputStream class, which provides the basic functionality for all output streams. The methods defined in the OutputStream class are listed, along with their descriptions, in Table 18.2.
Method | Description |
write() | Writes data to the stream. |
flush() | Forces any buffered output to be written. |
close() | Closes the stream. |
void write(int b) void write(byte b[]) void write(byte b[], int off, int len)
The first version of the write() method simply writes a single byte to the stream, whereas the second version writes all the bytes contained in the given byte array. The third version enables your program to write data from a byte array, specifying a starting offset (off) for the write and the number of bytes to write (len).
The signatures for the flush() and close() look exactly as they're shown in Table 18.2.
In order to support the standard input and output devices (usually the keyboard and screen, respectively), Java defines two stream objects that you can use in your programs without having to create stream objects of your own. The System.in object (instantiated from the InputStream class) enables your programs to read data from the keyboard, whereas the System.out object (instantiated from the PrintStream class) routes output to the computer's screen. You can use these stream objects directly in order to handle standard input and output in your Java programs, or you can use them as the basis for other stream objects you may want to create.
For example, Listing 18.1 is a Java application that accepts a line of input from the user and then displays the line on the screen. Figure 18.1 shows the application running in a DOS window. FIG. 18.1 Java's System class provides for standard I/O.
import java.io.*; class IOApp { public static void main(String args[]) { byte buffer[] = new byte[255]; System.out.println("\nType a line of text: "); try { System.in.read(buffer, 0, 255); } catch (Exception e) { String err = e.toString(); System.out.println(err); } System.out.println("\nThe line you typed was: "); String inputStr = new String(buffer, 0); System.out.println(inputStr); } }
You probably noticed in Listing 18.1 a method called println(), which is not a part of the OutputStream class. In order to provide for more flexible output on the standard output stream, the System class derives its out output-stream object from the PrintStream class, which provides for printing values as text output. Table 18.3 lists the methods of the PrintStream class, along with their descriptions.
Method | Description |
write() | Writes data to the stream. |
flush() | Flushes data from the stream. |
checkError() | Flushes the stream, returning errors that occurred. |
print() | Prints data in text form. |
println() | Prints a line of data (followed by a newline character) in text form. |
close() | Closes the stream. |
void write(int b) void write(byte b[], int off, int len) void print(Object obj) void print(String s) void print(char s[]) void print(char c) void print(int i) void print(long l) void print(float f) void print(double d) void print(boolean b) void println() void println(Object obj) void println(String s) void println(char s[]) void println(char c) void println(int i) void println(long l) void println(float f) void println(double d) void println(boolean b)
Now that you've had an introduction to the stream classes, you can put your knowledge to work. Perhaps the most common use of I/O--outside of retrieving data from the keyboard and displaying data on-screen--is file I/O. Any program that wants to retain its status (including the status of any edited files) must be capable of loading and saving files. Java provides several classes--including File, RandomAccessFile, FileInputStream, and FileOutputStream--for dealing with files. In this section, you examine these classes and get a chance to see how they work.
When you start reading and writing to a disk from a networked application, you have to consider security issues. Because the Java language is used especially for creating Internet-based applications, security is even more important. No user wants to worry that the Web pages he's currently viewing are capable of reading from and writing to his hard disk. For this reason, the Java system was designed such that the user can set system security from within his Java-compatible browser and so determine which files and directories are to remain accessible to the browser and which are to be locked up tight.
In most cases, the user disallows all file access on his local system, thus completely protecting his system from unwarranted intrusion. Because of this, virtually no applet relies on being able to create, read, or write files. This tight security is vital to the existence of applets because of the way they are automatically downloaded onto a user's system behind the user's back, as it were. No one would use Java-compatible browsers if they feared that such use would open their system to the tampering of nosy corporations and sociopathic programmers.
Java stand-alone applications, however, are a whole different story. Java applications are no different than any other application on your system. They cannot be automatically downloaded and run the way applets are. For this reason, stand-alone applications can have full access to the file system on which they are run. The file-handling examples in this chapter, then, are incorporated into Java stand-alone applications.
If your file-reading needs are relatively simple, you can use the FileInputStream class, which is a simple input-stream class derived from InputStream. This class features all the methods inherited from the InputStream class. To create an object of the FileInputStream class, you call one of its constructors, of which there are three, as shown:
FileInputStream(String name) FileInputStream(File file) FileInputStream(FileDescriptor fdObj)
The first constructor creates a FileInputStream object from the given file name name. The second constructor creates the object from a File object, and the third creates the object from a FileDescriptor object.
Listing 18.2 is a Java application that reads its own source code from disk and
displays the code on the screen. Figure 18.2 shows the application's output in a
DOS window.
FIG. 18.2
The FileApp application reads and displays its own source code.
import java.io.*; class FileApp { public static void main(String args[]) { byte buffer[] = new byte[2056]; try { FileInputStream fileIn = new FileInputStream("fileapp.java"); int bytes = fileIn.read(buffer, 0, 2056); String str = new String(buffer, 0, 0, bytes); System.out.println(str); } catch (Exception e) { String err = e.toString(); System.out.println(err); } } }
As you may have guessed, the counterpart to the FileInputStream class is FileOutputStream, which provides basic file-writing capabilities. Besides FileOutputStream's methods, which are inherited from OutputStream, the class features three constructors, whose signatures look like this:
FileOutputStream(String name) FileOutputStream(File file) FileOutputStream(FileDescriptor fdObj)
The first constructor creates a FileOutputStream object from the given file name, name, whereas the second constructor creates the object from a File object. The third constructor creates the object from a FileDescriptor object.
Listing 18.3 is a Java application that reads a line of text from the keyboard
and saves it to a file. When you run the application, type a line and press Enter.
Then at the system prompt (for DOS), type TYPE LINE.TXT to display the text
in the file, just to prove it's really there. Figure 18.3 shows a typical program
run.
FIG. 18.3
The FileApp2 application saves user input to a file.
import java.io.*; class FileApp2 { public static void main(String args[]) { byte buffer[] = new byte[80]; try { System.out.println ("\nEnter a line to be saved to disk:"); int bytes = System.in.read(buffer); FileOutputStream fileOut = new FileOutputStream("line.txt"); fileOut.write(buffer, 0, bytes); } catch (Exception e) { String err = e.toString(); System.out.println(err); } } }
If you need to obtain information about a file, you should create an object of Java's File class. This class enables you to query the system about everything from the file's name to the time it was last modified. You can also use the File class to make new directories, as well as to delete and rename files. You create a File object by calling one of the class's three constructors, whose signatures are:
File(String path) File(String path, String name) File(File dir, String name)
The first constructor creates a File object from the given full path name (for example, C:\CLASSES\MYAPP.JAVA). The second constructor creates the object from a separate path and a file, and the third creates the object from a separate path and file name, with the path being that associated with another File object.
The File class features a full set of methods that give your program lots of file-handling options. Table 18.4 lists these methods along with their descriptions.
Method | Description |
getName() | Gets the file's name. |
getPath() | Gets the file's path. |
getAbsolutePath() | Gets the file's absolute path. |
getParent() | Gets the file's parent directory. |
exists() | Returns true if the file exists. |
canWrite() | Returns true if the file can be written to. |
canRead() | Returns true if the file can be read. |
isFile() | Returns true if the file is valid. |
isDirectory() | Returns true if the directory is valid. |
isAbsolute() | Returns true if the file name is absolute. |
lastModified() | Returns the time the file was last changed. |
length() | Returns the length of the file. |
mkdir() | Makes a directory. |
renameTo() | Renames the file. |
mkdirs() | Creates a directory tree. |
list() | Gets a list of files in the directory. |
delete() | Deletes the file. |
hashCode() | Gets a hash code for the file. |
equals() | Compares the File object with another object. |
toString() | Gets a string containing the file's path. |
You may think, at this point, that Java's file-handling abilities are scattered through a lot of different classes, making it difficult to obtain the basic functionality you need to read, write, and otherwise manage a file. But Java's creators are way ahead of you. They created the RandomAccessFile class for those times when you really need to get serious about your file handling. By using this class, you can do just about everything you need to do with a file.
You create a RandomAccessFile object by calling one of the class's two constructors, whose signatures are:
RandomAccessFile(String name, String mode) RandomAccessFile(File file, String mode)
The first constructor creates a RandomAccessFile object from a string containing the file name and another string containing the access mode (" for read and rw for read and write). The second constructor creates the object from a File object and the mode string.
Once you have the RandomAccessFile object created, you can call upon the object's methods to manipulate the file. Those methods are listed in Table 18.5.
Method | Description |
close() | Closes the file. |
getFD() | Gets a FileDescriptor object for the file. |
getFilePointer() | Gets the location of the file pointer. |
length() | Gets the length of the file. |
read() | Reads data from the file. |
readBoolean() | Reads a boolean value from the file. |
readByte() | Reads a byte from the file. |
readChar() | Reads a char from the file. |
readDouble() | Reads a double floating-point value from the file. |
readFloat() | Reads a float from the file. |
readFully() | Reads data into an array, completely filling the array. |
readInt() | Reads an int from the file. |
readLine() | Reads a text line from the file. |
readLong() | Reads a long int from the file. |
readShort() | Reads a short int from the file. |
readUnsignedByte() | Reads an unsigned byte from the file. |
readUnsignedShort() | Reads an unsigned short int from the file. |
readUTF() | Reads a UTF string from the file. |
seek() | Positions the file pointer in the file. |
skipBytes() | Skips over a given number of bytes in the file. |
write() | Writes data to the file. |
writeBoolean() | Writes a boolean to the file. |
writeByte() | Writes a byte to the file. |
writeBytes() | Writes a string as bytes. |
writeChar() | Writes a char to the file. |
writeChars() | Writes a string as char data. |
writeDouble() | Writes a double floating-point value to the file. |
writeFloat() | Writes a float to the file. |
writeInt() | Writes an int to the file. |
writeLong() | Writes a long int to the file. |
writeShort() | Writes a short int to the file. |
writeUTF() | Writes a UTF string. |
import java.io.*; class FileApp3 { public static void main(String args[]) { try { RandomAccessFile file = new RandomAccessFile("fileapp3.java", "r"); long filePointer = 0; long length = file.length(); while (filePointer < length) { String s = file.readLine(); System.out.println(s); filePointer = file.getFilePointer(); } } catch (Exception e) { String err = e.toString(); System.out.println(err); } } }
Normal stream and file handling under Java isn't all that different than under any other computer language. The Java stream classes provide all the functions you're used to using to handle streams. However, Java also supports pipes, a form of data stream with which you may have little experience. Basically, pipes are a way to transfer data directly between different threads. One thread sends data through its output pipe, and another thread reads the data from its input pipe. By using pipes, you can share data between different threads without having to resort to things like temporary files.
As you may have guessed, Java provides two special classes for dealing with pipes. The first class, PipedInputStream, represents the input side of a pipe, and the second, PipedOutputStream, represents the output side of the pipe. These classes work together to provide a piped stream of data in much the same way a conventional pipe provides a stream of water. If you were to cap off one end of a conventional pipe, the flow of water would stop. The same is also true of piped streams. If you don't have both an input and output stream, you've effectively sealed off one or both of the ends of the data pipe.
To create a piped stream, you first create an object of the PipedOutputStream class. Then, you create an object of the PipedInputStream class, handing it a reference to the piped output stream, like this:
pipeOut = new PipedOutputStream(); pipeIn = new PipedInputStream(pipeOut);
By giving the PipedInputStream object a reference to the output pipe,
you've effectively connected the input and output into a stream through which data
can flow in a single direction. Data that's pumped into the output side of the pipe
can be received by another thread that has access to the input side of the pipe,
as shown in Figure 18.5.
FIG. 18.5
The output stream and input stream act as two ends on a one-way pipe.
NOTE: It may seem a little weird that the output side of the pipe is the side into which data is pumped, and the input side is the side from which the data flows. You have to think in terms of the threads that are using the pipe, rather than of the pipe itself. That is, the thread supplying data sends its output into the piped output stream, and the thread inputting the data takes it from the piped input stream.
Listings 18.5 through 18.7 are the source code for an application called PipeApp
that uses pipes to process data. The application has three threads: the main thread
plus two secondary threads that are started by the main thread. The program takes
a file that contains all Xs, and, using pipes to transfer data, first changes the
data to all Ys and finally changes the data to all Zs, after which the program displays
the modified data on-screen. Note that no additional files, beyond the input file,
are created. All data is manipulated using pipes. Figure 18.6 shows a program run.
FIG. 18.6
The PipeApp application uses pipes to share data with three threads.
import java.io.*; class PipeApp { public static void main(String[] args) { PipeApp pipeApp = new PipeApp(); try { FileInputStream XFileIn = new FileInputStream("input.txt"); InputStream YInPipe = pipeApp.changeToY(XFileIn); InputStream ZInPipe = pipeApp.changeToZ(YInPipe); System.out.println(); System.out.println("Here are the results:"); System.out.println(); DataInputStream inputStream = new DataInputStream(ZInPipe); String str = inputStream.readLine(); while (str != null) { System.out.println(str); str = inputStream.readLine(); } inputStream.close(); } catch (Exception e) { System.out.println(e.toString()); } } public InputStream changeToY(InputStream inputStream) { try { DataInputStream XFileIn = new DataInputStream(inputStream); PipedOutputStream pipeOut = new PipedOutputStream(); PipedInputStream pipeIn = new PipedInputStream(pipeOut); PrintStream printStream = new PrintStream(pipeOut); YThread yThread = new YThread(XFileIn, printStream); yThread.start(); return pipeIn; } catch (Exception e) { System.out.println(e.toString()); } return null; } public InputStream changeToZ(InputStream inputStream) { try { DataInputStream YFileIn = new DataInputStream(inputStream); PipedOutputStream pipeOut2 = new PipedOutputStream(); PipedInputStream pipeIn2 = new PipedInputStream(pipeOut2); PrintStream printStream2 = new PrintStream(pipeOut2); ZThread zThread = new ZThread(YFileIn, printStream2); zThread.start(); return pipeIn2; } catch (Exception e) { System.out.println(e.toString()); } return null; } }
import java.io.*; class YThread extends Thread { DataInputStream XFileIn; PrintStream printStream; YThread(DataInputStream XFileIn, PrintStream printStream) { this.XFileIn = XFileIn; this.printStream = printStream; } public void run() { try { String XString = XFileIn.readLine(); while (XString != null) { String YString = XString.replace(`X', `Y'); printStream.println(YString); printStream.flush(); XString = XFileIn.readLine(); } printStream.close(); } catch (IOException e) { System.out.println(e.toString()); } } }
import java.io.*; class ZThread extends Thread { DataInputStream YFileIn; PrintStream printStream; ZThread(DataInputStream YFileIn, PrintStream printStream) { this.YFileIn = YFileIn; this.printStream = printStream; } public void run() { try { String YString = YFileIn.readLine(); while (YString != null) { String ZString = YString.replace(`Y', `Z'); printStream.println(ZString); printStream.flush(); YString = YFileIn.readLine(); } printStream.close(); } catch (IOException e) { System.out.println(e.toString()); } } }
Seeing the PipeApp application work and understanding why it works are two very different things. In this section you examine the program line by line in order to see what's going on. The PipeApp.java file is the main program thread, so you start your exploration there. This application contains three methods: the main() method, which all applications must have, and the changeToY() and changeToZ() methods, which start two additional threads.
Inside main(), the program first creates an application object for the program:
PipeApp pipeApp = new PipeApp();
This is necessary to be able to call the ChangeToY() and ChangeToZ() methods, which don't exist until the application object has been created. One way around this would be to make all the class's methods static, rather than just main(). Then, you could call the methods without creating an object of the class.
After creating the application object, the program sets up a try program block because streams require that IOException exceptions be caught in your code. Inside the try block, the program creates an input stream for the source text file:
FileInputStream XFileIn = new FileInputStream("input.txt");
This new input stream is passed to the changeToY() method so that the next thread can read the file:
InputStream YInPipe = pipeApp.changeToY(XFileIn);
The changeToY() method creates the thread that changes the input data to all Ys (you will see how this method works in the following section, "Exploring the changeToY() Method") and returns the input pipe from the thread. The next thread can use this input pipe to access the data created by the first thread. So the input pipe is passed as an argument to the changeToZ() method:
InputStream ZInPipe = pipeApp.changeToZ(YInPipe);
The changeToZ() method starts the thread that changes the data from all Ys to all Zs. The main program uses the input pipe returned from changeToZ() in order to access the modified data and print it on-screen.
After the program gets the ZInPipe piped input stream, it prints a message on-screen:
System.out.println(); System.out.println("Here are the results:"); System.out.println();
Then, the program maps the piped input stream to a DataInputStream object, which enables the program to read the data using the readLine() method:
DataInputStream inputStream = new DataInputStream(ZInPipe);
Once the input stream is created, the program can read the data in, line by line, and display it on-screen (see Listing 18.8).
String str = inputStream.readLine(); while (str != null) { System.out.println(str); str = inputStream.readLine(); }
Finally, after displaying the data, the program closes the input stream:
inputStream.close();
Inside the changeToY() method is the first place in the program you really get to see pipes in action. Like main(), the changeToY() method does most of its processing inside a try program block to catch IOException exceptions. The method first maps the source input stream, which was passed as the method's single parameter, to a DataInputStream object. This enables the program to read data from the stream using the readLine() method:
DataInputStream XFileIn = new DataInputStream(inputStream);
Next, changeToY() creates the output pipe and input pipe:
PipedOutputStream pipeOut = new PipedOutputStream();
PipedInputStream pipeIn = new PipedInputStream(pipeOut);
Then, in order to be able to use the println() method to output text lines to the pipe, the program maps the output pipe to a PrintStream object:
PrintStream printStream = new PrintStream(pipeOut);
At this point, the method has four streams created:
Figure 18.7 illustrates this situation.
Now the program can create the thread that changes the data from Xs to Ys. That thread is an object of the YThread class, whose constructor is passed the input file (XFileIn) and the output pipe (now called printStream) as arguments:
YThread yThread = new YThread(XFileIn, printStream);
After creating the thread, the program starts the thread:
yThread.start();
As you soon see, the YThread thread reads data in from XFileIn,
changes the data from Xs to Ys, and outputs the result into printStream,
which is the output end of the pipe. Because the output end of the pipe is connected
to the input end (pipeIn), the input end contains the data that the YThread
thread changed to Ys. The program returns that end of the pipe from the changeToY()
method so that it can be used as the input for the changeToZ() method. Figure
18.8 shows the changeToY() portion of the chain.
FIG. 18.7
These are the streams created in the changeToY() method.
FIG. 18.8
The changeToY() method reads in Xs and send Ys into the pipe.
The changeToZ() method works similarly to the changeToY() method. However, because the way each method accesses its streams is important to understanding the PipeApp application, you examine changeToZ() line by line, too. The changeToZ() method starts by mapping its input stream, which is the input end of the pipe returned from changeToY(), to a DataInputStream object so that the program can read from the stream using the readLine() method:
DataInputStream YFileIn = new DataInputStream(inputStream);
The program then creates a new pipe:
PipedOutputStream pipeOut2 = new PipedOutputStream(); PipedInputStream pipeIn2 = new PipedInputStream(pipeOut2);
This new pipe routes data from the third thread (counting the main thread) back to the main program.
After creating the pipe, the program maps the output end to a PrintStream object so that data can be sent into the pipe using the println() method:
PrintStream printStream2 = new PrintStream(pipeOut2);
Next, the program creates a thread from the ZThread class, providing the input pipe created by changeToY() and the new output pipe (mapped to printStream2) as arguments to the class's constructor:
ZThread zThread = new ZThread(YFileIn, printStream2);
The next line starts the thread:
zThread.start();
The ZThread thread reads data from the input pipe created by changeToY()
that was stuffed with data by the YThread thread, then changes the data
to Zs, and finally outputs the data to the output pipe called printStream2.
The changeToZ() method returns the input half of this pipe (pipeIn2)
from the method, where the main program prints the stream's contents on-screen. You
now have a stream scenario like that illustrated in Figure 18.9.
FIG. 18.9
The data travels a long path as it's changed from all Ys to all Zs.
You now should have a basic understanding of how the pipes work. The last part of the puzzle is the way that the secondary threads, YThread and ZThread, service the pipes. Because the two threads work almost identically, you examine only YThread.
YThread's constructor receives two parameters: the input file and the output end of the first pipe. The constructor saves these parameters as data members of the class:
this.XFileIn = XFileIn; this.printStream = printStream;
With its streams in hand, the thread can start processing the data, which it does in its run() method. First, the thread reads a line from the input file; then it starts a while loop that processes all the data in the file. The first line read from the file before the loop begins ensures that XString is not null, which would prevent the loop from executing:
String XString = XFileIn.readLine(); while (XString != null)
Inside the loop, the thread first changes the newly read data to all Ys:
String YString = XString.replace(`X', `Y');
It then outputs the modified data to the output end of the pipe:
printStream.println(YString); printStream.flush();
It's important to flush the stream to ensure that all buffered data has been output into the pipe.
Next, the thread reads another line of data for the next iteration of the loop:
XString = XFileIn.readLine();
Finally, when the loop completes, the thread closes the piped output stream:
printStream.close();
And that's all there is to it. To put it simply, the thread does nothing more than read lines from the input file, change the characters in the lines to Ys, and ship the changed data into the pipe, from which it is retrieved from the next thread.
The ZThread thread works almost exactly the same way, except its input stream is the input end of the pipe into which yThread output its data. Finally, the input end of zThread's pipe feeds the main program as the program reads the text lines and displays them on-screen.