Section 11.3. Case Study: Reading and Writing Text Files


[Page 518 (continued)]

11.3. Case 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 (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 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 


End-of-file character



[Page 519]

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 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:

1.

Connect an output stream to the file.

2.

Write text data into the stream, possibly using a loop.

3.

Close the stream.

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

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.


Code 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.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.


[Page 520]

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

Even though Java will close any open files and streams when a program terminates normally, it is good programming practice to close the file yourself with a close() statement. This reduces the chances of damaging the file if the program terminates abnormally.



[Page 521]

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.

private void writeTextFile(JTextArea display, String fileName) {   try {       FileWriter outStream =  new FileWriter (fileName);       outStream.write (display.getText());       outStream.close();   } catch (IOException e) {       display.setText("IOERROR: " + e.getMessage() + "\n");       e.printStackTrace();   } } // writeTextFile() 

11.3.3. Code Reuse: Designing Text File Output

The 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:

  • What methods do we need to perform the desired task?

  • What streams contain the desired methods?

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

Developing effective I/O routines is primarily a matter of choosing the right library methods. Start by asking yourself, what methods you 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 (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:


[Page 522]

//  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

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. The same approach can be used to help you design I/O methods for other tasks.

java.sun.com/docs


Self-Study Exercise

Exercise 11.1

Is it possible to perform output to a text file using a PrintWriter and a FileWriter stream in combination? If so, write the Java code.

11.3.4. Reading 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:

1.

Connect an input stream to the file.

2.

Read the text data using a loop.

3.

Close the stream.


[Page 523]

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

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.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.
(This item is displayed on page 524 in the print version)


As shown in Figure 11.10, 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 read-Line() 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. 11.10). Can we somehow use it? Fortunately, the answer is yes. BufferedReader's constructor takes a Reader object as a parameter. But a FileReader is a Readerthat is, it is a descendant of the Reader class. So, using our plumbing analogy again, to build an input stream to the file, we can join a BufferedReader and a FileReader


[Page 524]

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.

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 right.

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:

String line = inStream.readLine(); while (line != null) {     display.append(line + "\n");     line = inStream.readLine(); } 


Using the end-of-file character


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 is why we add a \n before we append the line to the JTextArea.


[Page 525]

Java 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 \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.

Figure 11.11. A method for reading a text file.

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.



[Page 526]

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.


Self-Study Exercise

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); 


11.3.5. Code 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 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



[Page 527]

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


Self-Study Exercise

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) 


11.3.6. The TextIO Application

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.


[Page 528]

Figure 11.12. The TextIO class.


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.

Figure 11.13. An application that performs simple text I/O.


Figure 11.14. The TextIO class.

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); 
[Page 529]
commands.add(read); commands.add(write); display.setLineWrap(true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add("North", commands); this.getContentPane().add( new JScrollPane(display)); this.getContentPane().add("Center", display); } // TextIO() private void writeTextFile(JTextArea display, String fileName) { try { FileWriter outStream = new FileWriter (fileName); outStream.write (display.getText()); outStream.close(); } catch (IOException e) { display.setText("IOERROR: " + e.getMessage() + "\n"); e.printStackTrace(); } } // writeTextFile() 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() public void actionPerformed(ActionEvent evt) { String fileName = nameField.getText(); if (evt.getSource() == read) { display.setText(""); readTextFile(display, fileName); } else writeTextFile(display, fileName); } // actionPerformed() public static void main(String args[]) { TextIO tio = new TextIO(); tio.setSize(400, 200); tio.setVisible(true); tio.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); // Quit the application } }); } // main() } // TextIO class




Java, Java, Java(c) Object-Orienting Problem Solving
Java, Java, Java, Object-Oriented Problem Solving (3rd Edition)
ISBN: 0131474340
EAN: 2147483647
Year: 2005
Pages: 275

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net