[ Previous Section | Next Section | Chapter Index | Main Index ]

Section 11.2

Files


The data and programs in a computer's main memory survive only as long as the power is on. For more permanent storage, computers use files, which are collections of data stored on a hard disk, on a USB memory stick, on a CD-ROM, or on some other type of storage device. Files are organized into directories (also called folders). A directory can hold other directories, as well as files. Both directories and files have names that are used to identify them.

Programs can read data from existing files. They can create new files and can write data to files. In Java, such input and output can be done using I/O streams. Human-readable character data can be read from a file using an object belonging to the class FileReader, which is a subclass of Reader. Similarly, data can be written to a file in human-readable format through an object of type FileWriter, a subclass of Writer. For files that store data in machine format, the appropriate I/O classes are FileInputStream and FileOutputStream. In this section, I will only discuss character-oriented file I/O using the FileReader and FileWriter classes. However, FileInputStream and FileOutputStream are used in an exactly parallel fashion. All these classes are defined in the java.io package.


11.2.1  Reading and Writing Files

The FileReader class has a constructor which takes the name of a file as a parameter and creates an input stream that can be used for reading from that file. This constructor will throw an exception of type FileNotFoundException if the file doesn't exist. For example, suppose you have a file named "data.txt", and you want your program to read data from that file. You could do the following to create an input stream for the file:

FileReader data;   // (Declare the variable before the
                   //   try statement, or else the variable
                   //   is local to the try block and you won't
                   //   be able to use it later in the program.)
                        
try {
   data = new FileReader("data.txt");  // create the stream
}
catch (FileNotFoundException e) {
   ... // do something to handle the error—maybe, end the program
}

The FileNotFoundException class is a subclass of IOException, so it would be acceptable to catch IOExceptions in the above try...catch statement. More generally, just about any error that can occur during input/output operations can be caught by a catch clause that handles IOException.

Once you have successfully created a FileReader, you can start reading data from it. But since FileReaders have only the primitive input methods inherited from the basic Reader class, you will probably want to wrap your FileReader in a Scanner, in a BufferedReader, or in some other wrapper class. (See the previous section for a discussion of BufferedReader and Scanner.) To create a BufferedReader for reading from a file named data.dat, you could say:

BufferedReader data;

try {
   data = new BufferedReader( new FileReader("data.dat") );
}
catch (FileNotFoundException e) {
   ... // handle the exception
}

Wrapping a Reader in a BufferedReader lets you easily read lines of text from the file, and the buffering can make the input more efficient.

To use a Scanner to read from the file, you can construct the scanner in a similar way. However, it is more common to construct it more directly from an object of type File (to be covered below):

Scanner in;

try {
   in = new Scanner( new File("data.dat") );
}
catch (FileNotFoundException e) {
   ... // handle the exception
}

Working with output files is no more difficult than this. You simply create an object belonging to the class FileWriter. You will probably want to wrap this output stream in an object of type PrintWriter. For example, suppose you want to write data to a file named "result.dat". Since the constructor for FileWriter can throw an exception of type IOException, you should use a try..catch statement:

PrintWriter result;

try {
   result = new PrintWriter(new FileWriter("result.dat"));
}
catch (IOException e) {
   ... // handle the exception
}

However, as with Scanner, it is more common to use a constructor that takes a File as parameter; this will automatically wrap the File in a FileWriter before creating the PrintWriter:

PrintWriter result;

try {
   result = new PrintWriter(new File("result.dat"));
}
catch (IOException e) {
   ... // handle the exception
}

You can even use just a String as the parameter to the constructor, and it will be interpreted as a file name (but you should remember that a String in the Scanner constructor does not name a file; instead the scanner will read characters from the string itself).

If no file named result.dat exists, a new file will be created. If the file already exists, then the current contents of the file will be erased and replaced with the data that your program writes to the file. This will be done without any warning. To avoid overwriting a file that already exists, you can check whether a file of the same name already exists before trying to create the stream, as discussed later in this section. An IOException might occur in the PrintWriter constructor if, for example, you are trying to create a file on a disk that is "write-protected," meaning that it cannot be modified.

When you are finished with a PrintWriter, you should call its flush() method, such as "result.flush()", to make sure that all the output has been sent to its destination. If you forget to do this, you might find that some of the data that you have written to a file output stream has not actually shown up in the file.

After you are finished using a file, it's a good idea to close the file, to tell the operating system that you are finished using it. You can close a file by calling the close() method of the associated PrintWriter, BufferedReader, or Scanner. Once a file has been closed, it is no longer possible to read data from it or write data to it, unless you open it again as a new I/O stream. (Note that for most I/O stream classes, including BufferedReader the close() method can throw an IOException, which must be handled; however, PrintWriter and Scanner override this method so that it cannot throw such exceptions.) If you forget to close a file, the file will ordinarily be closed automatically when the program terminates or when the file object is garbage collected, but it is better not to depend on this. Note that calling close() should automatically call flush() before the file is closed. (I have seen that fail, but not recently.)

As a complete example, here is a program that will read numbers from a file named data.dat, and will then write out the same numbers in reverse order to another file named result.dat. It is assumed that data.dat contains only real numbers. The input file is read using a Scanner. Exception-handling is used to check for problems along the way. Although the application is not a particularly useful one, this program demonstrates the basics of working with files.

import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;

/**
 * Reads numbers from a file named data.dat and writes them to a file
 * named result.dat in reverse order.  The input file should contain
 * only real numbers.
 */
public class ReverseFileWithScanner {

    public static void main(String[] args) {

        Scanner data;        // For reading the data.
        PrintWriter result;  // Character output stream for writing data.

        ArrayList<Double> numbers;  // An ArrayList for holding the data.

        numbers = new ArrayList<Double>();

        try {  // Create the input stream.
            data = new Scanner(new File("data.dat"));
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file data.dat!");
            return;  // End the program by returning from main().
        }

        try {  // Create the output stream.
            result = new PrintWriter("result.dat");
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't open file result.dat!");
            System.out.println("Error: " + e);
            data.close();  // Close the input file.
            return;        // End the program.
        }

        while ( data.hasNextDouble() ) {  // Read until end-of-file.
            double inputNumber = data.nextDouble();
            numbers.add( inputNumber );
        }

        // Output the numbers in reverse order.

        for (int i = numbers.size()-1; i >= 0; i--)
            result.println(numbers.get(i));

        System.out.println("Done!");

        data.close();
        result.close();

    }  // end of main()

} // end class ReverseFileWithScanner

Note that this program will simply stop reading data from the file if it encounters anything other than a number in the input. That will not be considered to be an error.


As mentioned at the end of Subsection 8.3.2, the pattern of creating or opening a "resource," using it, and then closing the resource is a very common one, and the pattern is supported by the syntax of the try..catch statement. Files are resources in this sense, as are Scanner, PrintWriter, and all of Java's I/O streams. All of these things define close() methods, and it is good form to close them when you are finished using them. Since they all implement the AutoCloseable interface, they are all resources in the sense required by try..catch. A try..catch statement can be used to automatically close a resource when the try statement ends, which eliminates the need to close it by hand in a finally clause. This assumes that you will open the resource and use it in the same try..catch.

As an example, the sample program ReverseFileWithResources.java is another version of the example we have been looking at. In this case, try..catch statements using the resource pattern are used to read the data from a file and to write the data to a file. My original program opened a file in one try statement and used it in another try statement. The resource pattern requires that it all be done in one try, which requires some reorganization of the code (and can sometimes make it harder to determine the exact cause of an exception). Here is the try..catch statement from the sample program that opens the input file, reads from it, and closes it automatically.

try( Scanner data = new Scanner(new File("data.dat")) ) {
        // Read numbers, adding them to the ArrayList.
    while ( data.hasNextDouble() ) {  // Read until end-of-file.
        double inputNumber = data.nextDouble();
        numbers.add( inputNumber );
    }
}
catch (FileNotFoundException e) {
        // Can be caused if file does not exist or can't be read.
    System.out.println("Can't open input file data.dat!");
    System.out.println("Error: " + e);
    return;  // Return from main(), since an error has occurred.
}

The resource, data, is constructed on the first line. The syntax requires a declaration of the resource with an initial value, in parentheses after the word "try." It's possible to have several resource declarations, separated by semicolons. They will be closed in the order opposite to the order in which they are declared.


11.2.2  Files and Directories

The subject of file names is actually more complicated than I've let on so far. To fully specify a file, you have to give both the name of the file and the name of the directory where that file is located. A simple file name like "data.dat" or "result.dat" is taken to refer to a file in a directory that is called the current directory (also known as the "default directory" or "working directory"). The current directory is not a permanent thing. It can be changed by the user or by a program. Files not in the current directory must be referred to by a path name, which includes both the name of the file and information about the directory where it can be found.

To complicate matters even further, there are two types of path names, absolute path names and relative path names. An absolute path name uniquely identifies one file among all the files available to the computer. It contains full information about which directory the file is in and what the file's name is. A relative path name tells the computer how to locate the file starting from the current directory.

Unfortunately, the syntax for file names and path names varies somewhat from one type of computer to another. Here are some examples:

When working on the command line, it's safe to say that if you stick to using simple file names only, and if the files are stored in the same directory with the program that will use them, then you will be OK. Later in this section, we'll look at a convenient way of letting the user specify a file in a GUI program, which allows you to avoid the issue of path names altogether.

It is possible for a Java program to find out the absolute path names for two important directories, the current directory and the user's home directory. The names of these directories are system properties, and they can be read using the function calls:

To avoid some of the problems caused by differences in path names between platforms, Java has the class java.io.File. An object belonging to this class does not actually represent a file! Precisely speaking, an object of type File represents a file name rather than a file as such. The file to which the name refers might or might not exist. Directories are treated in the same way as files, so a File object can represent a directory just as easily as it can represent a file.

A File object has a constructor, "new File(String)", that creates a File object from a path name. The name can be a simple name, a relative path, or an absolute path. For example, new File("data.dat") creates a File object that refers to a file named data.dat, in the current directory. Another constructor, "new File(File,String)", has two parameters. The first is a File object that refers to a directory. The second can be the name of the file in that directory or a relative path from that directory to the file.

File objects contain several useful instance methods. Assuming that file is a variable of type File, here are some of the methods that are available:

Here, for example, is a program that will list the names of all the files in a directory specified by the user. In this example, I have used a Scanner to read the user's input:

import java.io.File;
import java.util.Scanner;

/**
 * This program lists the files in a directory specified by
 * the user.  The user is asked to type in a directory name.
 * If the name entered by the user is not a directory, a
 * message is printed and the program ends.
 */
public class DirectoryList {

   
   public static void main(String[] args) {
   
      String directoryName;  // Directory name entered by the user.
      File directory;        // File object referring to the directory.
      String[] files;        // Array of file names in the directory.
      Scanner scanner;       // For reading a line of input from the user.

      scanner = new Scanner(System.in);  // scanner reads from standard input.

      System.out.print("Enter a directory name: ");
      directoryName = scanner.nextLine().trim();
      directory = new File(directoryName);
      
      if (directory.isDirectory() == false) {
          if (directory.exists() == false)
             System.out.println("There is no such directory!");
          else
             System.out.println("That file is not a directory.");
      }
      else {
          files = directory.list();
          System.out.println("Files in directory \"" + directory + "\":");
          for (int i = 0; i < files.length; i++)
             System.out.println("   " + files[i]);
      }
   
   } // end main()

} // end class DirectoryList

All the classes that are used for reading data from files and writing data to files have constructors that take a File object as a parameter. For example, if file is a variable of type File, and you want to read character data from that file, you can create a FileReader to do so by saying new FileReader(file).


11.2.3  File Dialog Boxes

In many programs, you want the user to be able to select the file that is going to be used for input or output. If your program lets the user type in the file name, you will just have to assume that the user understands how to work with files and directories. But in a graphical user interface, the user expects to be able to select files using a file dialog box, which is a window that a program can open when it wants the user to select a file for input or output. JavaFX includes a platform-independent technique for using file dialog boxes in the form of a class called FileChooser, in package javafx.stage.

A file dialog box shows the user a list of files and sub-directories in some directory, and makes it easy for the user to specify a file in that directory. The user can also navigate easily from one directory to another. The constructor for FileChooser has no parameter. Constructing a FileChooser object does not make the dialog box appear on the screen. You have to call a method in the object to do that. Often, before showing the dialog box, you will call instance methods in the FileChooser object to set some properties of the dialog box. For example, you can set the file name that is shown to the user as a default initial value for the file.

A file dialog box can have an "owner," which is a window. In JavaFX, that means an object of type Stage. Until the dialog box is dismissed by the user—either by canceling the dialog or selecting a file—all interaction with the owner window is blocked. The owner can be specified as a parameter to the method that opens the dialog. The owner can be null, which will mean that no window is blocked.

There are two types of file dialog: an open file dialog that allows the user to specify an existing file to be opened for reading data into the program; and a save file dialog that lets the user specify a file, which might or might not already exist, to be opened for output. A FileChooser has two instance methods for showing the two kinds of dialog box on the screen. Suppose that fileDialog is a variable of type FileChooser. Then the following methods are available:

A typical program has "Save" and "Open" commands for working with files. When the user selects a file for saving or opening, it can be a good idea to store the selected File object in an instance variable. Later, that file can be used to initialize the directory and possibly the file name the next time a file dialog box is created. If editFile is the instance variable that records the selected file, and if it is non-null, then editFile.getName() is a String giving the name of the file, and editFile.getParent() is a File representing the directory that contains the file.

This leaves open one question: what to do when an error occurs while reading or writing the selected file? The error should be caught, and the user should be informed that an error occurred. In a GUI program, the natural way to do that is with another dialog box that shows an error message to the user and has an "OK" button for dismissing the dialog. Dialog boxes were not covered in Chapter 6, but some common simple dialog boxes can be shown using objects of type Alert, from package javafx.scene.control. (See Subsection 13.4.1 for more about alerts.) Here is how to show an error message to the user:

Alert errorAlert = new Alert( Alert.AlertType.ERROR, message );
errorAlert.showAndWait();

Putting all this together, we can look at a typical subroutine that saves data to a file. The file is selected using a FileChooser. In this example, the data is written in text form, using a PrintWriter:

private void doSave() {
    FileChooser fileDialog = new FileChooser(); 
    if (editFile == null) {
           // No file is being edited.  Set file name to "filename.txt"
           // and set the directory to the user's home directory.
        fileDialog.setInitialFileName("filename.txt");
        fileDialog.setInitialDirectory( 
                new File( System.getProperty("user.home")) );
    }
    else {
           // Get the file name and directory for the dialog from
           //       the file that is currently being edited.
        fileDialog.setInitialFileName(editFile.getName());
        fileDialog.setInitialDirectory(editFile.getParentFile());
    }
    fileDialog.setTitle("Select File to be Saved");
    File selectedFile = fileDialog.showSaveDialog(mainWindow);
    if ( selectedFile == null )
        return;  // User did not select a file.
    // Note: User has selected a file AND, if the file exists, has
    //    confirmed that it is OK to erase the exiting file.
    PrintWriter out; 
    try {
        FileWriter stream = new FileWriter(selectedFile); 
        out = new PrintWriter( stream );
    }
    catch (Exception e) {
           // Most likely, user doesn't have permission to write the file.
        Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                "Sorry, but an error occurred while\n" +
                trying to open the file for output.");
        errorAlert.showAndWait();
        return;
    }
    try {
           .
           .   // WRITE TEXT TO THE FILE, using the PrintWriter
           .
        out.flush(); // (not needed?; it's probably done by out.close();
        out.close();
        if (out.checkError())   // (need to check for errors in PrintWriter)
            throw new IOException("Error check failed.");
        editFile = selectedFile;
    }
    catch (Exception e) {
        Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                "Sorry, but an error occurred while\n" +
                "trying to write data to the file.");
        errorAlert.showAndWait();
    }    
}

This general outline can easily be adapted to non-text files by using a different type of output stream.

Reading data from a file is similar, and I won't show the corresponding doOpen() method here. You can find working subroutines for saving and opening text files in the sample program TrivialEdit.java, which lets the user edit small text files. The file subroutines in that program can be adapted to many GUI programs that work with files.


[ Previous Section | Next Section | Chapter Index | Main Index ]