Skip to main content

Section 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 (FigureĀ 11.3.1). Note that even this simple interface will let the user create new files and rename existing files.

Figure 11.3.1. The GUI design for a program that reads and writes text files.

Subsection 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

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.

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

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.

Subsection 11.3.3 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.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).

Figure 11.3.4. 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.

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.

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

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()
Listing 11.3.6. A method to write a text file.

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

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

Figure 11.3.8. The PrintWriter class.

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.

Figure 11.3.9. The FileOutputStream class.

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:

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.

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.

Exercises Self-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.

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

Figure 11.3.12. A stream serves as a pipe through which data flow.

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.

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.

Figure 11.3.14. FileReader's superclasses contain read() methods but no readLine() methods.

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:

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.

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.

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()
Listing 11.3.17. A method for reading a text file.

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.

Exercises Self-Study Exercises

1. Read Loop.

Organize the following blocks into a correctly formed read loop.

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

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.

Exercises Self-Study Exercise

1. Read Loop Characters.

Organize the following blocks into a correctly formed loop that will read a file character-by-character.

Subsection 11.3.7 The TextIOApplication

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.

Figure 11.3.23. The TextIO class.

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.

Figure 11.3.24. An application that performs simple text I/O.
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);
  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
Listing 11.3.25. The TextIO class.

Activity 11.3.1. TextIO application.

Try running the TextIO program in replitā€‰1ā€‰. 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.

You have attempted of activities on this page.
replit.com/@RalphMorelli/TextIO#Main.java