Hands On Java Lab 10 Exercise Lab 10: I/O Introduction Throughout this manual, we have made use of files -- containers on a hard or floppy disk that can be used to store information for long periods of time. For example, each source program that we have written has been stored in a file, and each executable program has also been stored in a file. While they may seem to be the same, files differ from programs in that a program is a sequence of instructions, and a file is a container in which a program (among other things) can be stored. In the previous exercise, we saw a different use for files is to store data. That is, where previously our exercises read data from the keyboard and wrote data to the screen, an alternative approach is to store the data to be read in a file, and read the data from there. Similarly, instead of writing data to the screen, there are situations where it is useful to instead have a program write its data to a file. This approach is particular useful for problems where the amount of data to be processed is so large that entering the data each time the program is executed (interactively) becomes inconvenient. That inconvenience can be eliminated by storing the data in a file, and then having the program read from the file, instead of the keyboard. In today's exercise we will take a closer look at the I/O facilities provided by Java. Streams A data stream is an abstraction of input/output that supports sequential reading/writing of data. For example, with an input stream, the data flows into the program and is handled by it in the order that it occurs in the stream. This notion fits rather closely with the kind of input we have done so far. In fact, Keyboard is a kind of input stream. Similarly, we have been writing to a Screen which is a kind of output stream. In fact, Java has two kinds of classes that encode the notion of a stream at a very primitive level. Byte Streams The most basic of the byte streams classes are the InputStream and OutputStream. These classes are very limited. Though there are a few other operations that are supported, they basically only know how to read and write bytes (8 bits of data). InputStream OutputStream read() - read a byte write() - write a byte close() - close the input stream close() - close the output stream One other disadvantage of the these streams is that they will perform an I/O operation with every read or write. This can be a serious problem if we are accessing a hard disk which has slow access times. To improve performance, most systems use some kind of buffering. For writing, data is kept in a buffer in RAM until a large chunk of data has been accumulated and then it is written as a group. For reading, data is read in a big chunk and placed in a buffer in RAM where it can be read one piece at a time. The goal of both of these processes is to reduce the number of times a program accesses the slower I/O hardware. Java encodes this notion in the two classes BufferedInputStream and BufferedOutputStream. They don't supply any additional operations, but are more efficient. If all we could do was to read and write bytes of data, we would not be very happy. Fortunately, we do not have to deal with such primitive capabilities. Java has stream objects that support abilities to write different kinds of data. These are the DataInputStream and the DataOutputStream. They can read and write any of the primitive data types that Java supports. Some of the additional methods that are available are: DataInputStream DataOutputStream readBoolean() - read a boolean writeBoolean() - write a boolean readByte() - read a byte writeByte() - write a byte readChar() - read a char writeChar() - write a char readDouble() - read a double writeDouble() - write a double readFloat() - read a float writeFloat() - write a float readInt() - read a int writeInt() - write a int readLong() - read a long writeLong() - write a long readShort() - read a short writeShort() - write a short These methods have been shown paired for a good reason. The write operations will record the binary representation of the data. The consequence of this is that these operations do not give us human readable output streams. If we want to read the data, we must use the corresponding read operation. If we were to do writeFloat() and then use readDouble() to try and retrieve the data, we will not read what we expect. Even worse, since float and double are different sizes, any further read operations will be out of synch with the subsequent data. Character Streams In addition, Java has another set of primitive stream classes for doing I/O that are based on the classes Reader and Writer. In contrast to the InputStream and OutputStream, these work with characters. Characters in Java are stored internally in Unicode which uses 16 bits to represent characters. The actual representation of the data that is stored in a text file will typically depend on the locality of the machine. Reader Writer read() - read a character write() - write a character close() - close the character stream close() - close the character stream Just as with byte streams, we have classes that support buffered I/O for character streams. These are BufferedReader and BufferedWriter. These classes support the reading and writing of strings of characters via the following operations. BufferedReader BufferedWriter readLine() - read a String of characters write() - overloaded to provide writing a String Though we will use one InputStream in today's lab, typically we work with character streams and that is where our focus will lie. Handling Output A print stream is a kind of stream that has been extended to support the operations print() and println(). These operations are overloaded so that they can successfully print an Object as well as all the primitive Java data types. In contrast to write(), these operations will print the data in human readable form. This should come as no surprise as you have used these operations many times. There are two kinds of print streams: PrintStream is a kind of OutputStream and PrintWriter is a kind of Writer. Predefined Streams There are three unique predefined streams in Java: System.in - This connection between your program and your computer's keyboard is a stream of type InputStream, so it only supports low level operations. System.out - This connection between your program and your computer's screen (or window) is a stream of type PrintStream, so it supports print(). This stream is used for to output normal text. System.err - This second connection between your program and your computer's screen (or window) is also a stream of type PrintStream. This stream is most commonly used to output error or diagnostic messages. While Java programmers will typically use System.out directly, they will rarely do so with System.in. Instead, they usually use System.in in the constructor of some other class that provides a more high level of access. Take a moment to look at the class definitions in ann.easyio. You'll see that the Screen class doesn't do much beyond invoking print() or println() with System.out. The Keyboard class on the other hand has added an operation that reads words and based on that are operations for reading each of the primitive data types. File Streams We want to be able to have the ability to connect a stream to a data file (open a file). Java has four basic classes that support this ability. Each of these classes has a constructor that accepts a string as the name of the file one wishes to open. File class Is a kind of FileInputStream InputStream FileOutputStream OutputStream FileReader Reader FileWriter Writer So if we needed to have a character stream connected to a file for reading, we would use FileReader. Since a FileReader is a kind of Reader, we can use it anywhere that an Reader can be used. If the file that we wanted to open was named "data.text" we would use the constructor new FileReader("data.text) to make the connection to the file. Each of the constructors that open a file for reading may throw a FileNotFoundException and we must put this code inside a try/catch which will catch that kind of exception, Similarly, if we open a file for writing, an IOException may be thrown and we must use a try/catch. Handling Input Java has a highly refined capability for writing data values in its print stream classes. For example, if we wanted to open a file for writing and use the print() method instead of write(), we could do PrintWriter out = null;
String name = "somewhere.dat";
try
{
out = new PrintWriter( // where print() is defined
new BufferedWriter( // buffer the output
new FileWriter(name))); // connect to the file
}
catch (IOException ex)
{
// code to deal with the exception
} With input, on the other hand, the best we have are the buffered streams which provide a readLine() facility. So how does one take that string and extract out the meaningful data? The answer is that we use a combination of a StringTokenizer and the classes associated with each primitive data type. The StringTokenizer is used to break the line up into the pieces that we will process individually. Examples of this are shown in GUI Interlude 2 and Lab 6. The following examples show how one can convert a String into an int and a float: String one = "123";
String two = "1.443";
int data1 = Integer.parseInt(one);
float data2 = Float.valueOf(two).floatValue();
float data3 = Float.parseDouble(two); For an int, we can use the parseInt() method, and for a float, we can use the parseDouble() method. Caesar Cipher When many of us were younger, we enjoyed writing secret messages, in which messages were encoded in such a way as to prevent others from reading them, unless they were in possession of a secret that enabled them to decode the message. Coded messages of this sort have a long history. For example, the Caesar cipher is a simple means of encoding messages that dates from Roman times. To illustrate, the Caesar cipher produces the encoded sentence:
Rqh li eb odqg, wzr li eb vhd.
when applied to the historic phrase:
One if by land, two if by sea.
What is the relationship between the letters in the original sentence and those in the encoded sentence? Today's exercise is to use the Caesar cipher to encode and decode messages stored in files. Getting Started Create a new project named Cipher for the files of this exercise, and in it, save copies of the files Encode.java, Decode.java, message.text, and alice.code. Then personalize the documentation in the file Encode.java, and take a few moments to study its contents. An Encoding Program The first part of today's exercise is to write a program that can be used to encode a message that is stored in a file. To demonstrate both input from and output to a file, we will store the encoded message in a second file. Design As usual, we will apply object-centered design to solve this problem. Behavior. Our program should display a greeting and then prompt for and read the name of the input file. It should then try to connect an input stream to that file so that we can read from it, and print a diagnostic message if the stream does not open correctly. It should then prompt for and read the name of the output file. It should then try to connect an output stream to that file so that we can write to it, and print a diagnostic message if the stream does not open correctly. For each character in the input file, our program should read the character, encode it using the Caesar cipher, and output the encoded character to the output file. Our program should conclude by disconnecting the streams from the files, and then print a "success" message. Objects. Using this behavioral description, we can identify the following objects: Description Type Kind Name An input stream BufferedReader varying theKeyboard An output stream PrintStream varying System.out a greeting String constant none The name of the input file String varying inFileName An input stream BufferedReader varying inStream The name of the output file String varying outFileName An output stream BufferedWriter varying outStream a character from the input file int varying inValue an encoded character char varying outChar Using this list of objects, we might specify the behavior of our program as follows:
Input(inFileName), a sequence of unencoded characters.
Output(outFileName), a sequence of encoded characters.
Operations. From our behavioral description, we have these operations: Description Defined? Name Package/Class? 1 Display a String yes println() java.io.PrintWriter 2 Read a String yes readLine() java.io.BufferedReader 3 Connect an input stream to a file yes BufferedReader constructor java.io.BufferedReader FileReader constructor java.io.FileReader 4 Connect an output stream to a file yes PrintWriter constructor java.io.PrintWriter FileWriter constructor java.io.FileWriter 5 Check that a stream opened properly yes An IOException is thrown if it does not 6 Read a char from an input stream yes read() java.io.BufferedReader 7 Encode a char using the Caesar cipher yes caesarEncode() Encode.java 8 Write a char to an output stream yes write() java.io.PrintWriter 9 Repeat 6, 7, 8 for each char in the file yes input loop built-in 10 Determine when all chars have been read yes read() returns -1 when no chars remain to be read 11 Disconnect a stream from a file yes close() java.io.BufferedReader java.io.PrintWriter Algorithm. We can organize these operations into the following algorithm:
0. Create an input stream to the keyboard using System.in.
1. Display a greeting.
2. Prompt for and read inFileName, the name of the input file.
3. Create a BufferedReader named inStream connecting
our program to inFileName.
4. Check that inStream opened correctly.
5. Prompt for and read outFileName, the name of the output file.
6. Create a BufferedWriter named outStream connecting
our program to outFileName.
7. Check that outStream opened correctly.
8. Loop through the following steps:
a. read a character from the input file.
b. if end-of-file was reached, then terminate repetition.
c. encode the character.
d. write the encoded character to the output file.
End loop.
9. Close the input and output connections.
10. Display a "successful completion" message.
Coding Encode.java already implements a number of these steps. It should be evident that we can perform step 1 with a relatively familiar output statement, step 8 using a forever loop, containing an if-break combination in step 8b to control the repetition, and step 8c using the caesarEncode() method that follows the main function. That leaves the file-related operations in steps 0, 2, 3, 4, 5, 6, 7, 8a, 8d and 9 for us to learn how to perform. Wrapping System.in with a BufferedReader As mentioned previously, System.in only provides a relatively low level access. We would like to create a BufferedReader based on System.in. The only problem with this is that both of the constructors for BufferedReader take an argument that is a Reader. So first we need to create a Reader using System.in. If we look at the documentation for Reader we see that it has a subclass (also acceptable) which is an InputStreamReader. InputStreamReader is a bridge class between streams and readers and has a constructor that will take an InputStream. We do new InputStreamReader(System.in) to create something which is a Reader. We take that and use it to create the BufferedReader: BufferedReader theKeyboard = new BufferedReader(
new InputStreamReader(System.in)); Using this information, implement step 0 of our algorithm in Encode.java. Reading a Line from a BufferedReader One might think that inFileName = theKeyboard.readLine(); would be sufficient to read in the name of the file. Unfortunately, if we try to compile this code, we will get an error message. This code has the possibility of throwing an IOException. Unlike the exceptions we've seen before which we can choose to ignore and let the code fail, Java forces us to deal with this type of exception. So we need to wrap this code in a try/catch as shown here: try{
inFileName = theKeyboard.readLine();
}
catch (IOException ex) {
System.err.println("Failed to read file name");
System.exit(1);
} Using this information, implement step 2 of our algorithm in Encode.java. Opening a Connection to a File. An executing program is unable to interact directly with a file for a very simple reason: an executing program resides in main memory and a file resides on a secondary memory device, such as a hard disk. However, an executing program can interact indirectly with a file, by opening a connection between the program and that file. In Java, such connections are FileReader or FileWriter objects as mentioned before. Like any other object, an FileReader must be created before it can be used. If inputFileName is a String object containing the name of an input file, then the code
new FileReader(inputFileName);
constructs a FileReader object connected to that file. Again we will want to wrap it with a BufferedReader for efficiency. new BufferedReader(
new FileReader(inputFileName)); Using this information, implement step 3 of our algorithm in Encode.java by declaring a BufferedReader named inStream that serves as a connection between our program and the file whose name is in inFileName. To perform step 5 of our algorithm, we must open a FileWriter for output to the output file. Such an object can be created as follows:
new FileWriter(outputFileName)
Similarly, we will want to use this object to create a BufferedWriter for efficiency. Using this information, implement step 5 of our algorithm by declaring an BufferedWriter named outStream that serves as a connection between our program and the file whose name is in outFileName. Checking that a Connection Opened Correctly. Opening files is an operation that is highly susceptible to user errors. For example, suppose the user has accidentally deleted the input file and our program tries to open a connection to it? In Java, if there is a problem opening a file to be read, a FileNotFoundException will be thrown. Again this exception requires our attention in the form of a try/catch. If there is a problem opening a file to be written, an IOException will be thrown, Complete steps 4 and 7 by wrapping the creation of inFileName an outFileName with try/catches. If there is an exception, print an error message and exit the program. When your source program compiles correctly (except possibly for an error indicating that the completion message statement may not be reached) continue on to the next part of the exercise. Do not execute your source program yet or an infinite loop will occur. Input from a BufferedReader. We are interested in reading one character at a time. The read() method of the BufferedReader class almost does what we want. It will read a character from the file, but it returns an int value (32 bit). This operation is described as reading from the file, even though we are actually operating on the BufferedReader.
intVariable = inStream.read();
Once again, if there is an error, an IOException will be thrown and we are forced to implement a try/catch to handle this. Using this information, implement step 8a of our algorithm. Then compile your program, and continue when what you have written is syntactically correct. Controlling a File-Input Loop. Files are created by a computer's operating system. When the operating system creates a file, it marks the end of the file with a special end-of-file mark. Input operations are then implemented in such a way as to prevent them from reading beyond the end-of-file mark, since doing so could allow a programmer unauthorized access to the files of another programmer. This end-of-file mark can be used to control a loop that is reading data from the file. Java indicates an end of file by having read() return the value -1. If you do a read after that, an exception will be generated. The expression
inValue == -1 will allow us to determine if the end of the file has been reached. In a forever loop like the one in the source program, we can prevent infinite loop behavior by placing an if-break combination:
if ( /* end-of-file has been reached */ ) break;
following the input step, repetition will be terminated when all of the data in the file has been processed. In your source program, place an if-break combination in the appropriate place to perform step 8b of our algorithm. Then compile your source program, to check the syntax of what you have written. At this point, you should not have any syntactical errors left in your program. When it is correct, continue to the next part of the exercise. File Output. We need to be able to write a character on the output file. If we examine the operations in BufferedWriter, we see that there is a write() method that will write a single character. This is just what we need. If we needed to be able to use print(), we can wrap our FileWriter with a PrintWriter instead of the BufferedWriter. The general form to use write() is:
outputStreamName.write(charValue) ;
where outputStreamName is a FileWriter or BufferedWriter, and charVariable is a character we wish to store in the file to which outputStreamName is a connection. Once again, if there is a problem, an IOException will be thrown which we must catch. Use this information to write the encoded character to your output file via outStream, to perform step 8d of our algorithm. Then compile your source program to test the syntax of what you have written, continuing when it is correct. Closing Files. Once we are done using a stream to read from or write to a file, we should close it, to break the connection between our program and the file. This is accomplished using the method close(), whose statement form is
streamName.close();
When execution reaches this statement, the program severs its connection to streamName. Once again, if there is a problem, an IOException will be thrown that must be dealt with. In the appropriate place in the source program, place calls to close() to break the connection between the program and the input file; and break the connection between the program and the output file. Then compile your source program, and ensure that it is free of syntax errors. Testing and Debugging When your program's syntax is correct, test it using the provided file named message.text. If what you have written is correct, your program should create an output file (e.g., message.code), containing the output:
Rqh Li Eb Odqg
Wzr Li Eb Vhd
If this file is not produced, then your program contains a logical error. Retrace your steps, comparing the statements in your source program to those described in the preceding parts of the exercise, until you find your error. Correct it, retranslate your source program and then retest your program, until it performs correctly. Applying What We Have Learned The last part of today's exercise is for you to apply what you have learned to the problem of decoding a file encoded using the Caesar cipher. Complete the skeleton program Decode.java, that can be used to decode a message encoded using the Caesar cipher. Do all that is necessary to get this program operational, so that messages encoded with Encode.java can be decoded with Decode.java. Put differently, the two programs should complement one another. To test your program, you can use the output file created by Encode.java, or alice.code, a selection from Lewis Carroll's Alice In WonderLand. Phrases you should now understand: File, Stream, Reader Writer, Buffer, Opening A File, File Input, File Output, Closing A File, End of File, IOException. Submit: Hard copies of your final versions of Encode.java and Decode.java, plus an execution record showing their execution. Back to This Lab's Table of Contents Back to the Prelab Questions Forward to the Homework Projects Back to the Table of Contents Back to the Introduction Copyright 2000 by Prentice Hall. All rights reserved.