11.3. Case Study: Reading and Writing Text FilesLet'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 (Fig. 11.5). Note that even this simple interface will let the user create new files and rename existing files. Figure 11.5. The GUI design for a program that reads and writes text files.
GUI design 11.3.1. Text File FormatA 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
End-of-file character 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 will 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. 11.3.2. Writing to a Text FileLet'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:
As Figure 11.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.
Output stream 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 may 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. Effective Design: Writing a File
Code Reuse: Designing an Output MethodNow 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.3 and Table 11.1. As we pointed out earlier, we are writing a text file, and therefore we must use a Writer subclass. But which subclass should we use? The only way to decide is to consult the Java API documentation, using links at
Choosing an output stream http://java.sun.com/j2se/docs/ to see what methods are available in the various subclasses. 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 (Fig. 11.6). Its name and description (Table 11.1) suggest that it is designed for writing text files. And indeed it contains the kind of constructor we needthat 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. Figure 11.6. To find the right I/O method, it is sometimes necessary to search the Java class hierarchy. This is easy to do with the online documentation.
However, FileWriter does not define a write() method. This doesn't necessarily mean that it does not contain such a method. Perhaps it has 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 (Fig. 11.6).
Inheritance 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) { // Create stream & open file FileWriter outStream = new FileWriter(fileName); // Write the entire display text and close the stream outStream.write(display.getText()); outStream.close(); // Close the output stream } 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. Java Programming Tip: Closing a File
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 (Fig. 11.7). Figure 11.7. A method to write a text file.
11.3.3. Code Reuse: Designing Text File OutputThe writeTextFile() method provides a simple example of how to write data to a text file. More important, its development illustrates the kinds of choices necessary to design effective I/O methods. Two important design questions we asked and answered were:
As in so many other examples we have considered, designing a method to perform a task is often a matter of finding the appropriate methods in the Java class hierarchy.
Method design Effective Design: Code Reuse
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 (Fig. 11.3 and Table 11.1). This class (Fig. 11.4) 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. So we must go back to searching Figure 11.3 and Table 11.1 for an appropriate candidate. Fortunately, the FileOutputStream class (Fig. 11.8) 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: // Create an output stream and open the file PrintWriter outStream = new PrintWriter(new FileOutputStream(fileName)); // Write the display's text and close the stream outStream.print ( display.getText() ); outStream.close(); Figure 11.8. The FileOutputStream class.
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.
Parameter agreement 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. Effective Design: Stream/Stream Connections
The important lesson here is that we found what we wanted by searching through the java.io.* hierarchy. The same approach can be used to help you design I/O methods for other tasks.
Self-Study Exercise
11.3.4. Reading from a Text FileLet'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:
As Figure 11.9 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, the file can only be read if it exists.) 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, the stream is closed once all the data are read. Figure 11.9. A stream serves as a pipe through which data flow.
Effective Design: Reading Data
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.3 and Table 11.1. 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.
Choosing an input stream What methods do we need? As in the preceding 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.
What methods should we use? The FileReader class (Fig. 11.10) has the right kind of constructor. However, it does not contain the readLine() methods that 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(); Figure 11.10. FileReader's superclasses contain read() methods but no readLine() methods. |
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 \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 Figure 11.11.
private void readTextFile(JTextArea display, String fileName) { try { BufferedReader inStream // Create and open the stream = new BufferedReader (new FileReader(fileName)); String line = inStream.readLine(); // Read one line while (line != null) { // While more text display.append(line + "\n"); // Display a line line = inStream.readLine(); // Read next line } inStream.close(); // Close the stream } catch (FileNotFoundException e) { display.setText("IOERROR: "+ fileName +" NOT found\n"); e.printStackTrace(); } catch ( IOException e ) { display.setText("IOERROR:" + e.getMessage() + "\n"); e.printStackTrace(); } } // readTextFile() |
Note that we must catch both the IOException, thrown by readLine() and close(), and the FileNotFoundException, thrown by the FileReader() constructor. It is important to make sure 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 }
IOException
When it attempts to read the end-of-file character, readLine() will return null.
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. |
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. |
Exercise 11.2 | What's wrong with the following loop for reading a text file and printing its output on the screen? String line = null; do { line = inStream.readLine(); System.out.println ( line ); } while (line != null); |
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 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.
Data conversion
A loop to read data from a file has the following basic form:
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 }
Effective Design: Read Loop Structure
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. |
Java Programming Tip: Read Versus Readline
Unless it is necessary to manipulate each character in the text file, reading a whole line at one 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 have developed can serve as suitable models for a wide variety of text I/O tasks, but if you find that they are not 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 read or write method you use.
Reusing existing code
Exercise 11.3 | What's wrong with the following loop for reading a text file and printing its output on the screen? int ch; do { ch = inStream.read(); System.out.print((char)ch); } while (ch != -1) |
Given the text I/O methods we wrote in the preceding sections, we can now specify the overall design of our TextIO class (Fig. 11.12). 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.13 shows how the finished product will look. The code is given in Figure 11.14. Pay particular attention to the actionPerformed() method, which uses the methods we defined in Section 11.3.5.
import javax.swing.*; // Swing components import java.awt.*; import java.io.*; import java.awt.event.*; public class TextIO extends JFrame implements ActionListener{ private JTextArea display = new JTextArea(); private JButton read = new JButton("Read From File"), write = new JButton("Write to File"); private JTextField nameField = new JTextField(20); private JLabel prompt = new JLabel("Filename:",JLabel.RIGHT); private JPanel commands = new JPanel(); public TextIO() { // Constructor super("TextIO Demo"); // Set window title read.addActionListener(this); write.addActionListener(this); commands.setLayout( new GridLayout(2,2,1,1)); // Control panel commands.add(prompt); commands.add(nameField); |