Section 10.3
Programming With Files
IN THIS SECTION, we look at several programming
examples that work with files. The techniques that we need were
introduced in Section 1 and Section 2.
The first example is a program that makes a list of all the
words that occur in a specified file. The user is asked to type
in the name of the file. The list of words is written to another
file, which the user also specifies. A word here just means a sequence
of letters. The list of words will be output in alphabetical order,
with no repetitions. All the words will be converted into lower case,
so that, for example, "The" and "the" will count as the same word.
Since we want to output the words in alphabetical order, we can't just
output the words as they are read from the input file. We can store the
words in an array, but since there is no way to tell in advance how many
words will be found in the file, we need a "dynamic array" which can
grow as large as necessary. Techniques for working with dynamic arrays
were discussed in Section 8.3. The data
is represented in the program by two static variables:
static String[] words; // An array that holds the words.
static int wordCount; // The number of words currently
// stored in the array.
The program starts with an empty array. Every time a word is read from
the file, it is inserted into the array (if it is not already there). The
array is kept at all times in alphabetical order, so the new word has to be
inserted into its proper position in that order. The insertion is done
by the following subroutine:
static void insertWord(String w) {
int pos = 0; // This will be the position in the array
// where the word w belongs.
w = w.toLowerCase(); // Convert word to lower case.
/* Find the position in the array where w belongs, after all the
words that precede w alphabetically. If a copy of w already
occupies that position, then it is not necessary to insert
w, so return immediately. */
while (pos < wordCount && words[pos].compareTo(w) < 0)
pos++;
if (pos < wordCount && words[pos].equals(w))
return;
/* If the array is full, make a new array that is twice as
big, copy all the words from the old array to the new,
and set the variable, words, to refer to the new array. */
if (wordCount == words.length) {
String[] newWords = new String[words.length*2];
System.arraycopy(words,0,newWords,0,wordCount);
words = newWords;
}
/* Put w into its correct position in the array. Move any
words that come after w up one space in the array to
make room for w. */
for (int i = wordCount; i > pos; i--)
words[i] = words[i-1];
words[pos] = w;
wordCount++;
} // end insertWord()
This subroutine is called by the main() routine of the program
to process each word that it reads from the file. If we ignore the
possibility of errors, an algorithm for the program is
Get the file names from the user
Create a TextReader for reading from the input file
Create a PrintWriter for writing to the output file
while there are more words in the input file:
Read a word from the input file
Insert the word into the words array
For i from 0 to wordCount - 1:
Write words[i] to the output file
Most of these steps can generate IOExceptions, and so they must
be done inside try...catch statements. In this case, we'll just
print an error message and terminate the program when an error occurs.
If in is the name of the TextReader that is being used
to read from the input file, we can read a word from the file with the
function in.getAlpha(). But testing whether there are any more words
in the file is a little tricky. The function in.eof() will check
whether there are any more non-whitespace characters in the file, but that's
not the same as checking whether there are more words. It might be that all
the remaining non-whitespace characters are non-letters. In that case,
trying to read a word will generate an error, even though in.eof()
is false. The fix for this is to
skip all non-letter characters before testing in.eof(). The
function in.peek() allows us to look ahead at the next character
without reading it, to check whether it is a letter. With this in mind,
the while loop in the algorithm can be written in Java as:
while (true) {
while ( ! in.eof() && ! Character.isLetter(in.peek()) )
in.getAnyChar(); // Read the non-letter character.
if ( in.eof() ) // End if there is nothing more to read.
break;
insertWord( in.getAlpha() );
}
With error-checking added, the complete main() routine is as follows.
If you want to see the program as a whole, you'll find the source code
in the file WordList.java.
public static void main(String[] args) {
TextReader in; // A stream for reading from the input file.
PrintWriter out; // A stream for writing to the output file.
String inputFileName; // Input file name, specified by the user.
String outputFileName; // Output file name, specified by the user.
words = new String[10]; // Start with space for 10 words.
wordCount = 0; // Currently, there are no words in array.
/* Get the input file name from the user and try to create the
input stream. If there is a FileNotFoundException, print
a message and terminate the program. */
TextIO.put("Input file name? ");
inputFileName = TextIO.getln().trim();
try {
in = new TextReader(new FileReader(inputFileName));
}
catch (FileNotFoundException e) {
TextIO.putln("Can't find file \"" + inputFileName + "\".");
return; // Returning from main() ends the program.
}
/* Get the output file name from the user and try to create the
output stream. If there is an IOException, print a message
and terminate the program. */
TextIO.put("Output file name? ");
outputFileName = TextIO.getln().trim();
try {
out = new PrintWriter(new FileWriter(outputFileName));
}
catch (IOException e) {
TextIO.putln("Can't open file \"" +
outputFileName + "\" for output.");
TextIO.putln(e.toString());
return;
}
/* Read all the words from the input stream and insert them into
the array of words. Reading from a TextReader can result in
an error of type TextReader.Error. If one occurs, print an
error message and terminate the program. */
try {
while (true) {
// Skip past any non-letters in the input stream. If
// end-of-stream has been reached, end the loop.
// Otherwise, read a word and insert it into the
// array of words.
while ( ! in.eof() && ! Character.isLetter(in.peek()) )
in.getAnyChar();
if (in.eof())
break;
insertWord(in.getAlpha());
}
}
catch (TextReader.Error e) {
TextIO.putln("An error occurred while reading from input file.");
TextIO.putln(e.toString());
return;
}
/* Write all the words from the list to the output stream. */
for (int i = 0; i < wordCount; i++)
out.println(words[i]);
/* Finish up by checking for an error on the output stream and
printing either a warning message or a message that the words
have been output to the output file. The PrintWriter class
does not throw an exception when an error occurs, so we have
to check for errors by calling the checkError() method. */
if (out.checkError() == true) {
TextIO.putln("Some error occurred while writing output.");
TextIO.putln("Output might be incomplete or invalid.");
}
else {
TextIO.putln(wordCount + " words from \"" + inputFileName +
"\" output to \"" + outputFileName + "\".");
}
} // end main()
Making a copy of a file is a pretty common operation, and most
operating systems already have a command for doing so. However, it
is still instructive to look at a Java program that does the same thing.
Many file operations are similar to copying a file, except that
the data from the input file is processed in some way before it is
written to the output file. All such operations can be done by
programs with the same general form.
Since the program should be able to copy any file, we can't assume
that the data in the file is in human-readable form. So, we have
to use InputStream and OutputStream to operate
on the file rather than Reader and Writer. The program
simply copies all the data from the InputStream to the OutputStream,
one byte at a time. If source is the variable that refers to
the InputStream, then the function source.read() can
be used to read one byte. This function returns the value -1 when all
the bytes in the input file have been read. Similarly, if copy
refers to the OutputStream, then copy.write(b) writes
one byte to the output file. So, the heart of the program is a simple while
loop. (As usual, the I/O operations can throw exceptions, so this must be
done in a try...catch statement.)
while(true) {
int data = source.read();
if (data < 0)
break;
copy.write(data);
}
The file-copy command in an operating system such as DOS or UNIX uses
command line arguments to specify the names of the files. For example,
the user might say "copy original.dat backup.dat" to copy an
existing file, original.dat, to a file named backup.dat.
Command-line arguments can also be used in Java programs. The command
line arguments are stored in the array of strings, args, which is
a parameter to the main() routine. The program can retrieve the
command-line arguments from this array. For example, if the program is
named CopyFile and if the user runs the program with the
command "java CopyFile work.dat oldwork.dat", then, in the program,
args[0] will be the string "work.dat" and args[1]
will be the string "oldwork.dat". The value of args.length
tells the program how many command-line arguments were specified by the user.
My CopyFile program gets the names of the files from the
command-line arguments. It prints an error message and exits if the
file names are not specified. To add a little interest, there are
two ways to use the program. The command line can simply specify the
two file names. In that case, if the output file already exists, the program
will print an error message and end. This is to make sure that the user
won't accidently overwrite an important file. However, if the command
line has three arguments, then the first argument must be "-f"
while the second and third arguments are file names. The -f is
a command-line option, which is meant to
modify the behavior of the program. The program interprets the -f
to mean that it's OK to overwrite an existing program. (The "f" stands
for "force," since it forces the file to be copied in spite of what would
otherwise have been considered an error.) You can see in the source code
how the command line arguments are interpreted by the program:
import java.io.*;
public class CopyFile {
public static void main(String[] args) {
String sourceName; // Name of the source file,
// as specified on the command line.
String copyName; // Name of the copy,
// as specified on the command line.
InputStream source; // Stream for reading from the source file.
OutputStream copy; // Stream for writing the copy.
boolean force; // This is set to true if the "-f" option
// is specified on the command line.
int byteCount; // Number of bytes copied from the source file.
/* Get file names from the command line and check for the
presence of the -f option. If the command line is not one
of the two possible legal forms, print an error message and
end this program. */
if (args.length == 3 && args[0].equalsIgnoreCase("-f")) {
sourceName = args[1];
copyName = args[2];
force = true;
}
else if (args.length == 2) {
sourceName = args[0];
copyName = args[1];
force = false;
}
else {
System.out.println(
"Usage: java CopyFile <source-file> <copy-name>");
System.out.println(
" or java CopyFile -f <source-file> <copy-name>");
return;
}
/* Create the input stream. If an error occurs,
end the program. */
try {
source = new FileInputStream(sourceName);
}
catch (FileNotFoundException e) {
System.out.println("Can't find file \"" + sourceName + "\".");
return;
}
/* If the output file already exists and the -f option was not
specified, print an error message and end the program. */
File file = new File(copyName);
if (file.exists() && force == false) {
System.out.println(
"Output file exists. Use the -f option to replace it.");
return;
}
/* Create the output stream. If an error occurs,
end the program. */
try {
copy = new FileOutputStream(copyName);
}
catch (IOException e) {
System.out.println("Can't open output file \""
+ copyName + "\".");
return;
}
/* Copy one byte at a time from the input stream to the output
stream, ending when the read() method returns -1 (which is
the signal that the end of the stream has been reached). If any
error occurs, print an error message. Also print a message if
the file has been copied successfully. */
byteCount = 0;
try {
while (true) {
int data = source.read();
if (data < 0)
break;
copy.write(data);
byteCount++;
}
source.close();
copy.close();
System.out.println("Successfully copied "
+ byteCount + " bytes.");
}
catch (Exception e) {
System.out.println("Error occurred while copying. "
+ byteCount + " bytes copied.");
System.out.println(e.toString());
}
} // end main()
} // end class CopyFile
Both of the previous programs use a command-line interface, but
graphical user interface programs can also manipulate files. Programs
typically have an "Open" command that reads the data from a file and
displays it in a window and a "Save" command that writes the data from
the window into a file. We can illustrate this in Java with a simple
text editor program. The window for this program uses a JTextArea
component to display some text that the user can edit. It also has a
menu bar, with a "File" menu that includes "Open" and "Save" commands.
To fully understand the examples in the rest of this section, you must be familiar with
the material on menus and frames from Section 7.7
and Section 7.7.
The examples also use file dialogs, which were introduced in
Section 2.
When the user selects the Save command from the File menu in the TrivialEdit program,
the program pops up a file dialog box where the user specifies
the file. The text from the JTextArea is written to the
file. All this is done in the following instance method
(where the variable, text, refers to the TextArea):
private void doSave() {
// Carry out the Save command by letting the user specify
// an output file and writing the text from the TextArea
// to that file.
File file; // The file that the user wants to save.
JFileChooser fd; // File dialog that lets the user specify the file.
fd = new JFileChooser(".");
fd.setDialogTitle("Save Text As...");
int action = fd.showSaveDialog(this);
if (action != JFileChooser.APPROVE_OPTION) {
// User has canceled, or an error occurred.
return;
}
file = fd.getSelectedFile();
if (file.exists()) {
// If file already exists, ask before replacing it.
action = JOptionPane.showConfirmDialog(this,
"Replace existing file?");
if (action != JOptionPane.YES_OPTION)
return;
}
try {
// Create a PrintWriter for writing to the specified
// file and write the text from the window to that stream.
PrintWriter out = new PrintWriter(new FileWriter(file));
String contents = text.getText();
out.print(contents);
if (out.checkError())
throw new IOException("Error while writing to file.");
out.close();
}
catch (IOException e) {
// Some error has occurred while trying to write.
// Show an error message.
JOptionPane.showMessageDialog(this,
"Sorry, an error has occurred:\n" + e.getMessage());
}
}
The methods JOptionPane.showConfirmDialog() and JOptionPane.showMessageDialog()
were discussed in Section 7.5.
When the user selects the Open command, a file dialog box allows the user to
specify the file that is to be opened. It is assumed that the file is a text
file. Since JTextAreas are not meant for displaying large amounts of
text, the number of lines read from the file is limited to one hundred at most.
Before the file is read, any text currently in the JTextArea is removed.
Then lines are read from the file and appended to the JTextArea one by one,
with a line feed character at the end of each line. This process continues
until one hundred lines have been read or until the end of the input file is
reached. If any error occurs during this process, an error message is
displayed to the user in a dialog box. Here is the complete method:
private void doOpen() {
// Carry out the Open command by letting the user specify
// the file to be opened and reading up to 100 lines from
// that file. The text from the file replaces the text
// in the JTextArea.
File file; // The file that the user wants to open.
JFileChooser fd; // File dialog that lets the user specify a file.
fd = new JFileChooser(new File("."));
fd.setDialogTitle("Open File...");
int action = fd.showOpenDialog(this);
if (action != JFileChooser.APPROVE_OPTION) {
// User canceled the dialog, or an error occurred.
return;
}
file = fd.getSelectedFile();
try {
// Read lines from the file until end-of-file is detected,
// or until 100 lines have been read. The lines are added
// to the JTextArea, with a line feed after each line.
TextReader in = new TextReader(new FileReader(file));
String line;
text.setText("");
int lineCt = 0;
while (lineCt < 100 && in.peek() != '\0') {
line = in.getln();
text.append(line + '\n');
lineCt++;
}
if (in.eof() == false)
text.append("\n\n****** Text truncated to 100 lines! ******\n");
in.close();
}
catch (Exception e) {
// Some error has occurred while trying to read the file.
// Show an error message.
JOptionPane.showMessageDialog(this,
"Sorry, some error occurred:\n" + e.getMessage());
}
}
The doSave() and doOpen() methods are the only part of the
text editor program that deal with files. If you would like to see the
entire program, you will find the source code in the file
TrivialEdit.java.
For a final example of files used in a complete program, you might want to look at
ShapeDrawWithFiles.java.
This file defines one last version of the ShapeDraw program, which you
last saw in Section 7.7.
This version has a "File" menu for
saving and loading the patterns of shapes that are created with
the program. The program also serves as an example of using
ObjectInputStream and ObjectOutputStream,
which were discussed at the end of Section 1.
If you check, you'll see that the Shape class in this
version has been declared to be Serializable so that
objects of type Shape can be written to and read from
object streams.