Section11.3CASE STUDY: Reading and Writing Text Files
Let’s write a GUI application that will be able to read and write data to and from a text file. To do this, we will need to develop a set of methods to perform I/O on text files.
The GUI for this application will contain a JTextArea, where text file data can be input and displayed, and a JTextField, where the user can enter the file’s name. It will also contain two JButtons, one for reading a file into the JTextArea, and the other for writing the data in the JTextArea into a file (Figure 11.3.1). Note that even this simple interface will let the user create new files and rename existing files.
Subsection11.3.1Text File Format
A text file consists of a sequence of characters divided into zero or more lines and ending with a special end-of-file character. When you open a new file in a text editor, it contains zero lines and zero characters. After typing a single character, it would contain one character and one line. The following would be an example of a file with four lines of text:
one\ntwo\nthree\nfour\n\eof
Note the use of the end-of-line character, \n, to mark the end of each line, and the use of the end-of-file character, \eof, to mark the end of the file. As we’ll see, the I/O methods for text files use these special characters to control reading and writing loops. Thus, when the file is read by appropriate Java methods, such as the BufferedReader.readLine() and BufferedReader.read() methods, one or more characters will be read until either an end-of-line or end-of-file character is encountered. When a line of characters is written using println(), the end-of-line character is appended to the characters themselves.
Subsection11.3.2Writing to a Text File
Let’s see how to write to a text file. In this program we write the entire contents of the JTextArea to the text file. In general, writing data to a file requires three steps:
Algorithm11.3.2.Writing to a file.
Connect an output stream to the file.
Write text data into the stream,possibly using a loop.
Close the stream.
As Figure 11.2.1 shows, connecting a stream to a file looks like doing a bit of plumbing. The first step is to connect an output stream to the file. The output stream serves as a conduit between the program and a named file. The output stream opens the file and gets it ready to accept data from the program. If the file already exists, then opening the file will destroy any data it previously contained. If the file doesn’t yet exist, then it will be created from scratch.
Once the file is open, the next step is to write the text to the stream, which passes the text on to the file. This step might require a loop that outputs one line of data on each iteration. Finally, once all the data have been written to the file, the stream should be closed. This also has the effect of closing the file itself.
Principle11.3.3.EFFECTIVE DESIGN: Writing a File.
Writing data to a file requires a three-step algorithm: (1) Connect an output stream to the file, (2) write the data, and (3) close the file.
Subsection11.3.3Code Reuse: Designing an Output Method
Now let’s see how these three steps are done in Java. Suppose the text we want to write is contained in a JTextArea. Thus, we want a method that will write the contents of a JTextArea to a named file.
What output stream should we use for the task of writing a String to a named file? To decide this, we need to use the information in Figure 11.2.4 and Table 11.2.7. As we pointed out earlier, because we’re writing a text file, we would use a Writer subclass. But which subclass should we use? One way to decide this is to consult the Java API documentation on the Oracle site.
For I/O operations you want to consult the classes in the java.io package. Ideally, we would like to be able to create an output stream to a named file, and we would like to be able to write a String to the file.
One likely candidate is the FileWriter class (Figure 11.3.4). Its name and description (Table 11.2.7) suggest that it’s designed for writing text files. And indeed it contains the kind of constructor we need—that is, one that takes the file name as a parameter. Note that by taking a boolean parameter, the second constructor allows us to append data to a file rather than rewrite the entire file, which is the default case.
However, FileWriter doesn’t have a write() method. This doesn’t necessarily mean that it doesn’t contain such a method. It might have inherited one from its superclasses, OutputStreamWriter and Writer. Indeed, the Writer class contains a method, write(), whose signature suggests that it is ideally suited for our task (Figure 11.3.4).
Having decided on a FileWriter stream, the rest of the task of designing our method is simply a matter of using FileWriter methods in an appropriate way:
private void writeTextFile(JTextArea display, String fileName) {
FileWriter outStream = new FileWriter(fileName); // Create stream & open file
outStream.write(display.getText()); // Write the entire display text
outStream.close(); // Close the stream amp;
}
We use the FileWriter() constructor to create an output stream to the file whose name is stored in fileName. In this case, the task of writing data to the file is handled by a single write() statement, which writes the entire contents of the JTextArea in one operation.
Finally, once we have finished writing the data, we close() the output stream. This also has the effect of closing the file. The overall effect of this method is that the text contained in display has been output to a file, named fileName, which is stored on the disk.
Principle11.3.5.PROGRAMMING TIP: Closing a File.
Even though Java will close any apen files and streams when a program terminates normally, it is good programming practice to close the file yourself with a close() statement. It also reduces the chances of damaging the file if the program terminates abnormally.
Because so many different things can go wrong during an I/O operation, most I/O operations generate some kind of checked exception. Therefore, it is necessary to embed the I/O operations within a try/catch statement. In this example, the FileWriter() constructor, the write() method, and the close() method may each throw an IOException. Therefore, the entire body of this method should be embedded within a try/catch block that catches the IOException(Listing 11.3.6).
Subsection11.3.4Code Reuse: Designing Text File Output
The writeTextFile() method provides a simple example of how to write data to a text file. More importantly, its development illustrates the kinds of choices necessary to design effective I/O methods. Two important design questions we asked and answered were
What methods do we need to perform the desired task?
What streams contain the desired methods?
As in so many other examples we’ve considered, designing a method to perform a task is often a matter of finding the appropriate methods in the Java class hierarchy.
Principle11.3.7.EFFECTIVE DESIGN: Code Reuse.
Developing effective I/O routines is primarily a matter of choosing the right library methods. Start by asking yourself, “What methods do I need?” and then find a stream class that contains the appropriate methods.
As you might expect, there is more than one way to write data to a text file. Suppose we decided that writing text to a file is like printing data to System.out. And suppose we chose to use a PrintWriter object as our first candidate for an output stream (Figure 11.3.8).
This class contains a wide range of print() methods for writing different types of data as text. So it has exactly the kind of method we need: print(String). However, this stream does not contain a constructor method that allows us to create a stream from the name of a file. Its constructors require either a Writer object or an OutputStream object.
This means that we can use a PrintWriter to print to a file, but only if we can first construct either an OutputStream or a Writer object to the file. Fortunately, the FileOutputStream class (Figure 11.3.9) has just the constructors we want. We now have an alternative way of coding the writeTextFile() method, this time using a combination of PrintWriter and FileOutputStream:
Algorithm11.3.10.Writing to a textfile.
PrintWriter outStream = // Create an output stream
new PrintWriter(new FileOutputStream(fileName)); // and open the file
outStream.print ( display.getText() ); // Write the text
outStream.close(); // Close the stream
Note how the output stream is created in this case. First, we create a FileOutputStream using the file name as its argument. Then we create a PrintWriter using the FileOutputStream as its argument. The reason we can do this is because the PrintWriter() constructor takes a FileOutputStream parameter. This is what makes the connection possible.
To use the plumbing analogy again, this is like connecting two sections of pipe between the program and the file. The data will flow from the program through PrintWriter, through the OutputStream, to the file. Of course, you can’t just arbitrarily connect one stream to another. They have to “fit together,” which means that their parameters have to match.
Two different kinds of streams can be connected if a constructor for one stream takes the second kind of stream as a parameter. This is often an effective way to create the kind of object you need to perform an I/O task.
The important lesson here is that we found what we wanted by searching through the java.io.* hierarchy. This same approach can be used to help you to design I/O methods for other tasks.
ExercisesSelf-Study Exercise
1.PrintWriter.
Which of the following code segments make proper use of PrintWriter (Figure 11.3.8) and a FileWriter (Figure 11.3.4) to perform output to a textfile.
PrintWriter outStream =
new FileWriter(new FileWriter(fileName));
outStream.print (display.getText());
outStream.close();
There is no FileWriter(OutputStream) constructor.
FileWriter outStream =
new FileWriter(new PrintWriter(fileName));
outStream.print (display.getText());
outStream.close();
There is no PrintWriter(fileName:String) constructor.
PrintWriter outStream =
new PrintWriter(new FileWriter(fileName));
outStream.print (display.getText());
outStream.close();
Yes, the FileWriter(filename: String) constructor will create an output stream for PrintWriter.
FileWriter outStream =
new PrintWriter(new FileWriter(fileName));
outStream.print (display.getText());
outStream.close();
PrintWriter is not a subclass of FileWriter.
Subsection11.3.5Reading from a Text File
Let’s now look at the problem of inputting data from an existing text file, a common operation that occurs whenever your email program opens an email message or your word processor opens a document. In general, there are three steps to reading data from a file:
Connect an input stream to the file.
Read the text data using a loop.
Close the stream.
As Figure 11.3.12 shows, the input stream serves as a kind of pipe between the file and the program. The first step is to connect an input stream to the file. Of course, in order to read a file, the file must exist. The input stream serves as a conduit between the program and the named file. It opens the file and gets it ready for reading. Once the file is open, the next step is to read the file’s data. This will usually require a loop that reads data until the end of the file is reached. Finally, once all the data are read, the stream should be closed.
Principle11.3.13.EFFECTIVE DESIGN: Reading Data.
Reading data from a file requires a three-step algorithm: (1) Connect an input stream to the file, (2) read the data, and (3) close the file.
Now let’s see how these three steps are done in Java. Suppose that we want to put the file’s data into a JTextArea. Thus, we want a method that will be given the name of a file and a reference to a JTextArea, and it will read the data from the file into the JTextArea.
What input stream should we use for this task? Here again we need to use the information in Figure 11.2.4 and Table 11.2.7. Because we’re reading a text file, we should use a Reader subclass. A good candidate is the FileReader, whose name and description suggest that it might contain useful methods.
What methods do we need? As in the previous example, we need a constructor method that connects an input stream to a file when the constructor is given the name of the file. And, ideally, we’d like to have a method that will read one line at a time from the text file.
The FileReader class (Figure 11.3.14) has the right kind of constructor. However, it contains no readLine() methods itself, which would be necessary for our purposes. Searching upward through its superclasses, we find that InputStreamReader , its immediate parent class, has a method that reads ints:
public int read() throws IOException();
As shown in Figure 11.3.14, this read() method is an override of the read() method defined in the Reader class, the root class for text file input streams. Thus, there are no readLine() methods in the Reader branch of the hierarchy. We have to look elsewhere for an appropriate class.
One class that does contain a readLine() method is BufferedReader(Fig. Figure 11.3.14). Can we somehow use it? Fortunately, the answer is yes. BufferedReader’s constructor takes a Reader object as a parameter. But a FileReaderis a Reader—that is, it is a descendant of the Reader class. So, to use our plumbing analogy again, to build an input stream to the file, we can join a BufferedReader and a FileReader
BufferedReader inStream
= new BufferedReader(new FileReader(fileName));
Given this sort of connection to the file, the program can use BufferedReader.readLine() to read one line at a time from the file.
So, we have found a method that reads one line at a time. Now we need an algorithm that will read the entire file. Of course, this will involve a loop, and the key will be to make sure we get the loop’s termination condition correct.
An important fact about readLine() is that it will return null as its value when it reaches the end of the file. Recall that text files have a special end-of-file character. When readLine() encounters this character, it will return null. Therefore, we can specify the following while loop:
Algorithm11.3.15.Reading a text file.
String line = inStream.readLine();
while (line != null) {
display.append(line + "\n");
line = inStream.readLine();
}
We begin outside the loop by attempting to read a line from the file. If the file happens to be empty (which it might be), then line will be set to null; otherwise it will contain the String that was read. In this case, we append the line to a JTextArea. Note that readLine()does not return the end-of-line character with its return value. That’s why we add a \n before we append the line to the JTextArea.
Principle11.3.16.PROGRAMMING TIP: End of Line.
Remember that readLine() does not return the end-of-line character as part of the text it returns. If you want to print the text on separate lines, you must append \(\backslash\)n.
The last statement in the body of the loop attempts to read the next line from the input stream. If the end of file has been reached, this attempt will return null and the loop will terminate. Otherwise, the loop will continue reading and displaying lines until the end of file is reached. Taken together, these various design decisions lead to the definition for readTextFile() shown in Listing 11.3.17.
Note that we must catch both the IOException, thrown by readLine() and close(), and the FileNotFoundException, thrown by the FileReader() constructor. It’s important to see that the read loop has the following form:
try to read one line of data and store it in line // Loop initializer
while ( line is not null ) { // Loop entry condition
process the data
try to read one line of data and store it in line // Loop updater
}
When it attempts to read the end-of-file character, readLine() will return null.
Principle11.3.18.EFFECTIVE DESIGN: Reading Text.
In reading text files, the readLine() method will return null when it tries to read the end-of-file character. This provides a convenient way of testing for the end of file.
Principle11.3.19.EFFECTIVE DESIGN: Reading an Empty File.
Loops for reading text files are designed to work even if the file is empty. Therefore, the loop should attempt to read a line before testing the loop-entry condition. If the initial read returns null, that means the file is empty and the loop body will be skipped.
ExercisesSelf-Study Exercises
1.Read Loop.
Organize the following blocks into a correctly formed read loop.
String line = inStream.readLine();
---
while (line != null)
---
{
---
display.append(line + "\n");
---
line = inStream.readLine();
---
}
Subsection11.3.6Code Reuse: Designing Text File Input
Our last example used BufferedReader.readLine() to read an entire line from the file in one operation. But this isn’t the only way to do things. For example, we could use the FileReader stream directly if we were willing to do without the readLine() method. Let’s design an algorithm that works in this case.
As we saw earlier, if you use a FileReader stream, then you must use the InputStreamReader.read() method. This method reads bytes from an input stream and translates them into Java Unicode characters. The read() method, for example, returns a single Unicode character as an int:
public int read() throws IOException();
Of course, we can always convert this to a char and concatenate it to a JTextArea, as the following algorithm illustrates:
int ch = inStream.read(); // Init: Try to read a character
while (ch != -1) { // Entry-condition: while more chars
display.append((char)ch + ""); // Append the character
ch = inStream.read(); // Updater: try to read
}
Although the details are different, the structure of this loop is the same as if we were reading one line at a time.
The loop variable in this case is an int because InputStreamReader.read() returns the next character as an int, or it returns \(-1\) if it encounters the end-of-file character. Because ch is an int, we must convert it to a char and then to a String in order to append() it to the display.
A loop to read data from a file has the following basic form:
Algorithm11.3.20.Read data loop.
try to read data into a variable // Initializer
while ( read was successful ) { // Entry condition
process the data
try to read data into a variable // Updater
}
The read() and readLine() methods have different ways to indicate when a read attempt fails. These differences affect how the loop-entry condition is specified, but the structure of the read loop is the same.
Principle11.3.22.PROGRAMMING TIP: Read Versus Readline.
Unless it is necessary to manipulate each character in the text file, reading a line at a time is more efficient and, therefore, preferable.
It is worth noting again the point we made earlier: Designing effective I/O routines is largely a matter of searching the java.io package for appropriate classes and methods. The methods we’ve developed can serve as suitable models for a wide variety of text I/O tasks, but if you find that they aren’t suitable for a particular task, you can design your own method. Just think about what it is you want the program to accomplish, then find the stream classes that contain methods you can use to perform the desired task. Basic reading and writing algorithms will be pretty much the same no matter which particular read or write method you use.
ExercisesSelf-Study Exercise
1.Read Loop Characters.
Organize the following blocks into a correctly formed loop that will read a file character-by-character.
Given the text I/O methods we wrote in the previous sections, we can now specify the overall design of our TextIO class (Figure 11.3.23). In order to complete this application, we need only set up its GUI and write its actionPerformed() method.
Setting up the GUI for this application is straightforward. Figure 11.3.24 shows how the finished product will look. The code is given in Listing 11.3.25. Pay particular attention to the actionPerformed() method, which uses the methods we defined in the previous section.
. As an input file you can use Testfile.txt, which is provided. Try making some edits and the saving (i.e., writing) and then reopening the file to see that your edits are saved.