14. Files and Streams: Input/Output Techniques

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

 

GUI design

 

 

 

 

Figure 11.5: The GUI design for a program that reads and writes


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 read- ing 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.

 

BorderLayout

 

text files.


JFrame


JLabel


JTextFieldJButtons


north


 

Component Hierarchy JFrame

Controls JPanel

Prompt JLabel Input JTextField ReadFile JButton WriteFile JButton

Display JTextArea

 

 

 

 

 

 

 

The end-of-file character


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:

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Output stream


J

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 appro- priate 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.

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:

Connect an output stream to the file.

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

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.

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.

 

 

 

 

 

 

 

 

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 Choosing an output stream

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, because we’re writing a text file, we would use a Writer subclass. But which subclass should we use? The only way to decide this is to consult the Java API documentation, using links at

,,

 

J

to see what methods are available in the various subclasses. For I/O op- erations 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’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 param- eter, 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 define 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 andInheritance

Writer. Indeed, the Writer class contains a method, write(), whose signature suggests that it is ideally suited for our task (Fig. 11.6).

 

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.

 

 

 

 

 

 

 

FileWriter

 

+FileWriter(in fileName : String)

+FileWriter(in fileName : String, in append :boolean)

 

Having decided on a FileWriter stream, the rest of the task of de- signing our method is simply a matter of using FileWriter methods in an appropriate way:

,,

 

 

 

 

 

J

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 (Fig. 11.7).

 

,,

 

 

 

 

 

 

 

 

J

Figure 11.7: A method to write a text file.

 

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 toMethod design

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 (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 con- structor 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 alterna- tive way of coding the writeTextFile() method, this time using a combination of PrintWriter and FileOutputStream:

 

Figure 11.8: The FileOutput- Stream class.


 

 

 

 

FileOutputStream

 

+FileOutputStream(in filename : String)

+FileOutputStream(in filename : String, in append : boolean)

 

,,

 

 

 

 

 

 

 

 

 

Parameter agreement

 

 

 

 

 

 

 

 

 

 

 

java.sun.com/j2se/1.5.0/docs/api/


J

Note how the output stream is created in this case. First, we create a FileOutputStream using the file name as its argument. Then we cre- ate 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.

 

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

 

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.

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:

Connect an input stream to the file.

 

Read the text data using a loop.

Close the stream.

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

 

Memory


 

 

 

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

 

 

 

 

 

 

 

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 Choosing an input stream

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.

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

structor is given the name of the file. And, ideally, we’d like to have a What methods should we use?

method that will read one line at a time from the text file.

The FileReader class (Fig. 11.10) 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:

,,

 

J

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 readLine() methods in the Reader branch of the hierarchy. We have to look elsewhere for an appropriate class.

 

Figure 11.10: FileReader’s su- perclasses contain read() meth- ods but no readLine() meth- ods.


Reader

 

+read() : int

 

 

 

 

 

 

 

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

,,

 

 

 

 

 

 

 

 

Using the end-of-file character


J

Given this sort of connection to the file, the program can use Buffered- Reader.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:

 

,,

 

 

 

J

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

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 11.11: A method for reading a text file.

 

Note that we must catch both the IOException, thrown by readLine() and close(), and the FileNotFoundException, thrownIOException by the FileReader() constructor. It’s important to see that the read loop

has the following form:

,,

 

 

 

 

 

J

 

When it attempts to read the end-of-file character, readLine() will return

null.

 

 

SELF-STUDY EXERCISE

EXERCISE 11.2What’s wrong with the following loop for reading a text file and printing its output on the screen?

,,

 

 

 

J

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:

,,

 

J

Of course, we can always convert this to a char and concatenate it to a

JTextArea, as the following algorithm illustrates:

,,

 

 

 

 

J

 

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 returns1 if it encoun- ters 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:


 

 

Data conversion

 

,,

 

 

 

J

 

 

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 ap- propriate 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 Reusing existing code

the same no matter which particular read or write method you use.

 

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?

,,

 

 

 

J

 

Figure 11.12: The TextIO class.


JFrame

 

 

 

 

 

 

 

TextIO

-display : JTextArea

-read : JButton

-nameField : JTextField

-prompt : JLabel

-commands : JPanel

+TextIO()

-readTextFile(in display : JTextArea, in fileName : String)

-writeTextFile(in display : JTextArea, in fileName : String)

+actionPerformed(in evt : ActionEvent)

+main(in args[] : String)

 

 

 

The TextIO Application

 

Given the text I/O methods we wrote in the previous 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 Fig- ure 11.14. Pay particular attention to the actionPerformed() method, which uses the methods we defined in the previous section.

 

 

 

 

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

 

,,

import j avax . swing . ;// S w i n g c o m p o n e n t s

import j ava . awt . ;

import j ava . io . ;

import j ava . awt . event . ;

public c l a s s TextIO extends JFrame implements Action Listener

private JText Area display = new JText Area ( ) ;

private JButton read = new JButton ( ”Read From F i l e ) ,

write = new JButton ( Write to F i l e ) ;

private J T e x t F i e l d nameField = new J T e x t F i e l d ( 2 0 ) ;

private JLabel prompt = new JLabel ( Filename : , JLabel . RIGHT ) ;

private JPanel commands = new JPanel ( ) ;

 

public TextIO ( )// C o n s t r u c t o r

super ( TextIO Demo” ) ;// S e t wi n do w t i t l e

read . add Action Listener ( t h i s ) ; write . add Action Listener ( t h i s ) ;

commands . set Layout ( new GridLayout ( 2 , 2 , 1 , 1 ) ) ;// C o n t r o l p a n e l

commands . add ( prompt ) ; commands . add ( nameField ) ; commands . add ( read ) ; commands . add ( write ) ;

display . setLineWrap ( t rue ) ;

t h i s . get Content Pane ( ) . set Layout ( new BorderLayout ( ) ) ;

t h i s . get Content Pane ( ) . add ( North” , commands ) ;

t h i s . get Content Pane ( ) . add ( new JS c r o l l P a n e ( display ) ) ;

t h i s . get Content Pane ( ) . add ( Center , display ) ;

} // T e x t I O

private void w r i t e T e x t F i l e ( JText Area display , S t r i n g fileName ) {

t r y F{i l e W r i t e r outStream =new F i l e W r i t e r ( fileName ) ;

outStream . write ( display . get Text ( ) ) ;

outStream . c l o s e ( ) ;

catch ( IOException e )

display . s e t T e xt ( ”IOERROR : + e . getMessage ( ) + n” ) ; e . pr int St ac k Tr ac e ( ) ;

}

} // w r i t e T e x t F i l e ( )

private void r e a d Te xt File ( JText Area display , S t r i n g fileName ) {

t r y B{uffe red Reader in Stream

// C r e a t e a n d o p e n t h e s t r e a m

= new Buffered Reader ( new File Reader ( fileName ) ) ; S t r i n g l i n e = in Stream . readLine ( ) ; // R e a d o n e l i n e while ( l i n e ! = null )// W h i l e m o r e t e x t

display . append ( l i n e + n” ) ; // D i s p l a y a l i n e

l i n e = in Stream . read Line ( ) ;// R e a d n e x t l i n e

} in Stream . c l o s e ( ) ;// C l o s e t h e s t r e a m

catch ( File Not FoundException e )

display . s e t T e xt ( ”IOERROR : + fileName +” NOT found n” ) ; e . pr int St ac k Tr ac e ( ) ;

catch ( IOException e )

display . s e t T e xt ( ”IOERROR : + e . getMessage ( ) + n” ) ; e . pr int St ac k Tr ac e ( ) ;

}

// r e a d T e x t F i l e

\J

Figure 11.14: Part I of the TextIO class.

 

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 11.14: (continued) The TextIO class, Part II.

 

 

 

 

 

 

 

home index.html


root


 

 

 

 

java examples


The
File Class

As we’ve seen, an attempt to create a FileReader stream may throw a FileNotFoundException. The way this happens is if the user provides a name for a file that either doesn’t exist or isn’t located where its name says it should be located. The question that needs to be considered: Is there any way we can detect these kinds of errors before attempting to

 

datafiles

data.txt


MyClass.java MyClass.class


read the file?

The java.io.File class provides methods that we can use for this task. The File class provides a representation of the computer’s file and

 

directory information in a platform-independent manner. As you know, a

file is a collection of data, whereas a directory is a collection of files. (To be

 

Figure 11.15: A simple hierarchy of directories and files.

 

 

 

 

 

 

 

 

 

The file hierarchy


exact, a directory is a file that stores its files’ names and attributes, not the files themselves.) In this section, we will provide details about the File class and how to use the methods available in the class.

Names and Paths

In order to correctly specify a file’s location, it is necessary to know a little about how files are stored on your computer’s disk drive. File systems are organized into a hierarchy. A path is a description of a file’s loca- tion in the hierarchy. For example, consider the hierarchy of files in Fig- ure 11.15. Assume that your Java program is named MyClass.class. When a program is running, the program’s directory is considered the current directory. Any files located in the current directory can be referred to by name alone—for example, MyClass.java. To refer to a file located in a subdirectory of the current directory, you need to provide the name of the subdirectory and the file: datafiles/data.txt. In this case, we are assuming a Unix file system, so we are using the / as the separator

 

SECTION 11.4 The File Class519

between the name of the directory (datafiles) and the name of the file (data.txt). This is an example of a relative path name, because we are specifying a file in relation to the current directory.

Alternatively, a file can be specified by its absolute path name. This would be a name whose path starts at the root directory of the file system. For example,

,,

 

 

 

would be the absolute path name for the file named data.txt on a Unix system. When you supply the name of a file to one of the stream construc- tors, you are actually providing a path name. If the path consists of just a name, such as data.txt, Java assumes that the file is located in the same directory as the program itself.

 

Validating File Names

Before reading a file it is often necessary to determine that the file’s name is a valid one and that the file can be read. The File class (Fig. 11.16) provides platform-independent methods for dealing with files and di- rectories. It contains methods that list the contents of directories, de- termine a file’s attributes, and rename and delete files. Note the sev- eral static constants provided. These allow path names to be speci- fied in a platform-independent way. For example, on a Unix system, the File.separator character will be the / and on a Windows system it will be the \,backslash. File.separator will be initialized to the appropriate separator for the particular system being used.


J

 

 

 

 

 

 

 

 

 

 

 

Figure 11.16: The java.io.File class.

 

 

 

 

 

 

 

 

 

 

 

 

 

As an example of how you might use some of File’s methods, let’s write a method that tests whether the file name entered by the user is the name of a valid, readable file.

A file might be unreadable for a number of reasons. It might be owned

by another user and readable only by that user. Or it might be designatedMethod design

as not readable by its owner. We’ll pass the method the name of the file (a String), and the method will return true if a readable file with that

 

name exists. Otherwise, the method will throw an exception and return

false:

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

J

The method simply creates a File instance and uses its exists() and canRead() methods to check whether its name is valid. If either condi- tion fails, an exception is thrown. The method handles its own exceptions, printing an error message and returning false in each case.

Before attempting to write data to a file, we might want to check that the file has been given an appropriate name. For example, if the user leaves the file name blank, we should not write data to the file. Also, a file might be designated as unwriteable in order to protect it from being in- advertently overwritten. We should check that the file is writeable before attempting to write to it:

,,

 

 

 

 

 

 

 

 

 

 

J

The first check in this code tests that the user has not forgotten to pro- vide a name for the output file. It is unlikely that the user wants to name the file with the empty string. We use the exists() method to test

 

whether the user is attempting to write to an existing file. If so, we use the canWrite() method to test whether the file is writeable. Both kinds of errors result in IOExceptions.

SELF-STUDY EXERCISE

EXERCISE 11.4 The other methods of the File class are just as easy to use as the ones we have illustrated in this section. Write a method that takes the name of a file as its single parameter and prints the following information about the file: its absolute path, its length, and whether it is a directory or a file.