Section 8.3
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 the computer's hard disk, on a floppy disk, on a CD-ROM, or on some other type of storage device. Files are organized into directories (sometimes 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, input and output is generally done using streams. Data is read from a file using an object belonging to the class FileInputStream, which is a subclass of InputStream. Similarly, data is written to a file through an object of type FileOutputStream, a subclass of OutputStream.

It's worth noting right at the start that applets which are downloaded over a network connection are generally not allowed to access files. This is a security consideration. You can download and run an applet just by visiting a Web page with your browser. If downloaded applets had access to the files on your computer, it would be easy to write an applet that would destroy all the data on any computer that downloads it. To prevent such possibilities, there are a number of things that downloaded applets are not allowed to do. Accessing files is one of those forbidden things. Standalone programs written in Java, however, have the same access to your files as any other program. When you write a standalone Java application, you can use all the file operations described in this section.


The FileInputStream 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. This exception type requires mandatory exception handling, so you have to call the constructor in a try statement (or inside a routine that is declared to throw FileNotFoundException). 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 as follows:

       FileInputStream data;   // declare the variable before the
                               //   try statement, or else the variable
                               //   is local to the try block
                               
       try {
          data = new FileInputStream("data.dat");  // create the stream
       }
       catch (FileNotFoundException e) {
          ... // do something to handle the error -- maybe, end the program
       }

Once you have successfully created a FileInputStream, you can start reading data from it. But since FileInputStreams have only the primitive input methods inherited from the basic InputStream class, you will probably want to wrap your FileInputStream in either a DataInputStream object or an AsciiInputStream object. You can use the built-in DataInputStream class if you want to read data in binary, machine-readable format. Use the non-standard class, AsciiInputStream, which was described in the previous section, if the data in the file is in human-readable ASCII-text format. To create an AsciiInputStream for reading from a file, you could say:

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

Once you have an AsciiInputStream named data, you can read from it using such methods as data.getInt() and data.getWord(), exactly as you would from any other AsciiInputStream.

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

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

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. An IOException might occur if, for example, you are trying to create a file on a disk that is "write-protected," meaning that it cannot be modified.

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. (If you forget to do this, the file will probably be closed automatically when the program terminates or when the file stream object is garbage collected, but it's best to close a file as soon as you are done with it.) You can close a file by calling the close() method of the associated file stream. 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 stream. (Note that for most stream classes, the close() method can throw an IOException, which must be handled; however, both PrintStream and AsciiInputStream override this method so that it cannot throw such exceptions.)

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 one number on each line, and that there are no more than 1000 numbers altogether. Exception-handling is used to check for problems along the way. At the end of this program, you'll find an example of the use of a finally clause in a try statement. When the computer executes a try statement, the commands in its finally clause are guaranteed to be executed, no matter what.

       import java.io.*;  
       // assume that the AsciiInputStream class is also available
       
       public class ReverseFile {
       
          public static void main(String[] args) {
          
             AsciiInputStream data;   // stream for reading data
             PrintStream result;      // stream for output
             
             double[] number = new double[1000];  // array to hold the numbers
                                                  //    read from the input file
             int numberCt;  // number of items stored in the array
             
             try {
                data = new AsciiInputStream(new FileInputStream("data.dat"));
             }
             catch (FileNotFoundException e) {
                System.out.println("Can't find file data.dat!");
                return;  // end the program by returning from main()
             }
 
             try {
                result = new PrintStream(new FileOutputStream("result.dat"));
             }
             catch (IOException e) {
                System.out.println("Can't open file result.dat!");
                System.out.println(e.toString());
                data.close();  // close the input file
                return;  // end the program
             }
             
             try {
             
                 // read the data from the input file,
                 numberCt = 0;
                 while (!data.eof()) {  // read to end-of-file
                    number[numberCt] = data.getlnDouble();
                    numberCt++;
                 }
              
                 // then output the numbers in reverse order
                 for (int i = numberCt-1; i >= 0; i--)
                    result.println(number[i]);

              }
              catch (AsciiInputException e) {
                 // some problem reading the data from the input file
                 System.out.println("Input Error: " + e.getMessage());
              }
              catch (IndexOutOfBoundsException e) {
                 // must have tried to put too many numbers in the array
                 System.out.println("Too many numbers in data file.");
                 System.out.println("Extras will be ignored.");
              }
              finally {
                 // finish by closing the files, whatever else may have happened
                 data.close();
                 result.close();
              }
                
          }  // end of main()
         
       } // end of class

File Names, Directories, and File Dialogs

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 (or "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 its 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 quite a bit from one type of computer to another. Here are some examples:

Similarly, the rules for determining which directory is the current directory are different for different types of computers. It's reasonably safe to say, though, 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.

In many cases, though, you would like the user to be able to select a file for input or output. If you let the user type in a 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 special window that a program can open when it wants the user to select a file for input or output. Java provides a platform-independent method for using file dialog boxes in the form of a class called FileDialog. This class is part of the package java.awt.

There are really two types of file dialog windows: one for selecting an existing file to be used for input, and one for specifying a file for output. You can specify which type of file dialog you want in the constructor for the FileDialog object, which has the form

        public FileDialog(Frame parent, String title, int mode)

where parent is meant to be the main application window (but can be null, at least on a Macintosh), title is meant to be a short string describing the dialog box, and mode is one of the constants FileDialog.SAVE or FileDialog.LOAD. Use FileDialog.SAVE if you want an output file, and use FileDialog.LOAD if you want a file for input. You can actually omit the mode parameter, which is equivalent to using FileDialog.LOAD.

Once you have a FileDialog, you can use its show() method to make it appear on the screen. It will stay on the screen until the user either selects a file or cancels the request. The instance method getFile() can then be called to retrieve the name of the file selected by the user. If the user has canceled the file dialog, then the String value returned by getFile() will be null. Since the user can select a file that is not in the current directory, you will also need directory information, which can be retrieved by calling the method getDirectory(). For example, if you want the user to select a file for output, and if the main window for your application is mainWin, you could say:

       FileDialog fd = new FileDialog(mainWin, "Select output file",
                                                      FileDialog.SAVE);
       fd.show();
       String fileName = fd.getFile();
       String directory = fd.getDirectory();

       if (fileName != null) {  // (otherwise, the user canceled the request)

          ...  // open the file, save the data, then close the file

       }

Once you have the file name and directory information, you will have to combine them into a usable file specification. The best way to do this is to create an object of type File. The file object can then be used as a parameter in a constructor for a FileInputStream or a FileOutputStream. For example, the body of the if statement in the above example could include:

        File file = new File(directory, fileName);
        PrintStream out = new PrintStream(new FileOutputStream(file));
        ... // write the data to the output stream, out
        out.close();

Of course, you'll have to do something about handling possible exceptions, in particular the IOException that could be generated by the constructor for the FileOutputStream. But for the most part, FileDialogs and streams provide a reasonably easy-to-use interface to the file system of any computer.


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