Monkeys at Keyboards: The Javanomicon
© Michael James Heron
Topic: Java Programming
Level: 2
Version: delta

Dulce et decorum est pro patria mori.
Horace

19 - File I/O 1

PreviousTable of ContentsNext
Forum


Chapter Objectives

By the end of this chapter, the reader will be able to:

  • define the need for File I/O
  • implement stream-based File I/O


19.1

Introduction

Up until this point, we have largely just been playing with Java. Although we have covered a large amount of the fundamental building blocks, we are hugely restricted in that we cannot possibly write 95% of real world applications, simply because we have no way of storing the state of an application between executions. We run our code, we put in some data, we close the application down - and everything is lost.

Until we learn how to deal with file input and output (henceforth referred to as File I/O), we will forever be left with a gaping hole in our knowledge. Lucky for us then that this chapter is all about that very subject!

The Java programming language provides us with a wide range of methods and objects for dealing with File I/O requirements. In this chapter, we'll be looking at one of the simpler frameworks for loading and saving data, that of Stream Based I/O. In the next chapter, we'll look at a somewhat more complicated way of saving data.

19.2

Java File I/O

Although it is possible to create an applet with File I/O capabilities, the applet sandbox (discussed in chapter 13 - Free Standing Applications) makes it a difficult affair. For the purposes of this module, we will be using the free standing application structure for all our file IO needs. This was also discussed in chapter 8.

Applets do allow for some degree of File IO, although it is subject to some rather significant restrictions. We will look at how we can make a connection to particular kinds of media files stored on disk in a later chapter on the subject of graphics and sound.

Java provides two main families of classes for file access. These are used for accessing files that belong to two main categories:

  • Stream Based Files, which are concerned with data that is organised sequentially. Text files are a familiar example of this kind of file... the order of the data is important.
  • Random Access Files, where the order of individual pieces of data is unimportant, and so particular sections can be extracted with no thought given to its context. Databases are a common example of this type of file.

Although databases are Random Access Files, we don't usually access them directly. Instead, we use a set of predefined classes (such as the JDBC framework). There do exist a number of situations where simple random access files are an appropriate form of storage, but we'll talk about that in the next chapter.

In this chapter we will concentrate on sequential access files, or Stream Based Files. These have the advantage of being conceptually simple, and the disadvantage of being somewhat unwieldy to manipulate. Provided the data being stored is of a format that is agreeable to stream based I/O, this isn't an issue.

19.3

Writing to Files

To start with, we'll look at how we write to a file from Java. Remember, we must use the free standing application structure, lest our pursuit for knowledge be rendered hopeless from the very start. With this caveat in mind, the process of writing a stream of data to a file follows a fairly simple structure:

  1. We open a connection to a file.
  2. We write some data to the file (often done in a loop)
  3. We close the file.

All of the classes we'll discuss during this chapter may be found in the java.io package, so this must be imported into your program before they will be recognised.

The first of these classes is a class called FileWriter. We create an instance of this in the same way we do all classes... we setup a variable of type FileWriter to hold the object:

FileWriter myWriter; 

And then we create an instance of the class and place it into this variable using the new keyword. In this case, the constructor method for the FileWriter class takes a single parameter - the filename that we wish to write to. If this file exists, the FileWriter object will make a connection to it. If it doesn't exist, it will first create the file and then make a connection:

myWriter = new FileWriter ("C:/bing"); 

The creation of this FileWriter object makes the connection to the file, and thus fulfils the first of our three requirements. But we still have to write some data to this file for step two, and here is where we encounter our first problem.

The FileWriter object is quite low level, which means that it provides a very limited set of methods for writing data, and this data is written in a very inefficient way. We can do some basic writing to the file using the write method:

myWriter.write ("Hello World!"); 

The write method allows us to write strings or arrays of characters to a file... or we can write individual Unicode values:

myWriter.write ('h'); 
myWriter.write ('e');
myWriter.write ('y');

This is a limited set of options and also somewhat inefficient since it writes data immediately after every invocation of the write method. Usually you don't need quite so rapid a turnaround in file IO, so it makes sense to wait until you have a number of things that need to be written, and write them in a single chunk. We can do this by using our FileWriter object as the basis for a more complex object called a BufferedWriter:

BufferedWriter myBuffered = new BufferedWriter (myWriter); 

Now we use our myBuffered object as the interface to the file and call our write operations on that, solving the inefficiency problem.

But it still isn't a particularly easy object to use for File IO since everything needs to be either in a string, in an array of characters, or an actual Unicode value. It would be nice if we had some way of building another object that had some more 'user friendly' methods. Luckily, we can do just that by using a PrintWriter object!

PrintWriter output = new PrintWriter (myBuffered); 

And voila! We get a set of nice methods for writing out data to a file. The main method is called println, which works very similarly to the System.out.println method except that it writes the text to a file on disk instead of the console.

The println method is extremely versatile and is happy to take doubles, floats, characters, strings, or even objects and write them to a file.

It is only by building up our objects by layering them that we get an efficient and usable object for file IO:

Classes involved in writing a stream-based file
Fig 19.1: Classes involved in writing a stream-based file

As mentioned above, File IO is often done in a loop... within this loop, all we need to put is the code for dealing with writing. We don't need to go through this layering approach each time (in fact, it would be very bad to do so). We create the object once, write to it until we're finished, and then finally we close the file by calling the close method:

FileWriter baseWriter = new FileWriter ("c:/bing.txt"); 
BufferedWriter buffer =new BufferedWriter (baseWriter);
PrintWriter out = new PrintWriter (buffer);
for (int i = 0; i < 10; i++) {
out.println ("Counted to " + i);
}

out.close();

Technically we can do file IO just fine with a simple FileWriter, but this will cause problems later on when developing more complicated File IO based programs and so it is a good idea for you to get the layering principle into your head sooner rather than later.

In an earlier chapter we discussed the idea of exceptions, and how some classes in Java have exceptions that must be acknowledged before the program will even compile. File IO is one of those areas that are full of mandatory acknowledgement exceptions, and so before we can compile a program that uses File IO we must try and catch the exceptions that are likely to occur.

Reading and writing data is a problematic area, not the least reason for which is because that it's possible, and in fact even common, that other things will be happening to a file outside of your Java program.

  • If you're on a multi-user system, someone else may be editing the file.
  • Your virus checker may be examining the file
  • Your disk drive may be undergoing defragmentation.

These are things that are beyond your ability to control as a Java developer, and so you are required to handle these exceptions before the program will run.

Exceptions are thrown when you create an instance of the FileWriter class (it throws an IOException when something goes awry) and when you call the close method on a FileWriter (also an IOException). Depending on what layers you have created around a FileWriter, you will either need to catch one or both of these.

Having gone through these steps, we can actually write an application that will write some text to a file... like so:

import java.io.*; 
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;


public class SimpleWritingApplication extends JFrame implements ActionListener {
JButton myButton;

public SimpleWritingApplication() {
myButton = new JButton ("Press me for File IO Goodness!");
add (myButton, BorderLayout.SOUTH);
myButton.addActionListener (this);
}


public static void main (String args[]) {
SimpleWritingApplication main = new SimpleWritingApplication();
main.setSize (300, 300);
main.setVisible (true);
}


public void actionPerformed (ActionEvent e) {
FileWriter baseWriter = null;
try {
baseWriter = new FileWriter ("bing.txt");
}

catch (IOException ex) {
JOptionPane.showMessageDialog (null, "A horrible error has occured!");
return;
}

BufferedWriter buffer = new BufferedWriter (baseWriter);
PrintWriter out = new PrintWriter (buffer);
out.println ("Goodbye cruel world!");
out.close();
}

}

And there we go! We press a button, and the application creates a file called bing.txt which contains the text:

Goodbye cruel world!

Flutes of warm, cheap champagne all around!! Our ship of discovery continues to sail the oceans of Java, finding islands simply awash with chests of golden treasures!

Java tip

Even applications that don't require saving the state of a user's interactions can benefit from file IO. You could for example add a simple method that logs some information to a standard text-file, and make use of that method any time an exception is caught to record what the state of the program was. In this way, you can provide a robust environment for the user and also an audit trail for debuggers.


There is a small problem with writing files however, in that not all systems use the \n notation to indicate a new line. Luckily the BufferedWriter class provides a method called newLine that handles this properly.

19.4

Reading From Files

Reading from files is a very similar procedure, which involves the same steps, although it requires a selection of different objects. We use a FileReader object to make a low level connection to the file, and then place a BufferedReader around this object to improve efficiency. There is little need to further layer objects around this, since stream based files have a simple structure that does not benefit from further abstraction.

The BufferedReader object has a method called readLine which reads a line of text from the file specified within the FileReader and then returns it as a string.

Unlike writing to a file, we must try and catch for an IOException around each invocation of the readLine method. Then, when we're done, we call the close method:

FileReader readBase; 
BufferedReader in;
String temp;
try {
readBase = new FileReader ("bing.txt");
in = new BufferedReader (readBase);
temp = in.readLine();
in.close();
}

catch (IOException except) {
JOptionPane.showMessageDialog (null, "A Horrible Error!");
}

System.out.println (temp);

As you can see, we follow a very similar set of steps. However, there is an additional complexity involved here which we will discuss in a few moments.

Unlike with a FileWriter, the FileReader requires the file to be there... it will not create one if no file exists. If the file does not exist, it will throw an exception. So let's read in the file that we created earlier (bing.txt). We create the connection to this file in the variable readBase, then put a BufferedReader around it. We then call readLine which reads a line of the text file into a variable called temp, and finally it closes the file before printing the text to the console.

So far, so good... but most text files have more than one line in them, and the readLine method doesn't seem to give a way to indicate which line we want to read!

This initial observation is entirely correct - reading through a file using this framework is a one way process (much like tokenizing a string). The reader object maintains a counter of where it is in the file, and each call of readLine returns the line of text at the current point before Java moves the counter on to the next line. Consider if we had a file that contained the following text:

Hello World.
Hello, hello!
HELLO!
Goodbye.

When we create our reader object, the counter starts at the first line:

* * Hello World. * *
Hello, hello!
HELLO!
Goodbye

Then when we call readLine, it returns the text at the current line (Hello World), and then moves onto the next line:

Hello World.
* * Hello, hello! * *
HELLO!
Goodbye

And so on with each subsequent call of readLine until it reaches the end of the file. If readLine is called after all the text has been read, it returns the value null.

We have no way of knowing in advance how many lines of text are going to be in a file, so a for loop won't work for us - a while loop is what we need to read in each line of text from a file. We continue to call readLine until what we get out of the call is the value null.

Exactly what we do with each line of text depends on what we're actually reading. If it's a simple text document, appending it to a string is a satisfactory operation. If we're doing something more complex, reading each line and adding it to an ArrayList may be preferable.

For this example, we'll just look at adding each line of text to a string. First we need a String variable to hold everything we've read... we'll call this variable input. We'll need another to hold the contents of the current line, and we'll call this temp:

String input = ""; 
String temp = "";

Then, we create a connection to the file as shown above:

readBase = new FileReader ("bing.txt"); 
in = new BufferedReader (readBase);

And now comes the bit where we need to do the reading from the file. First, we read a line from the file:

temp = in.readLine(); 

And then we start our loop. We want to continue reading from the file while temp is not null:

while (temp != null) { 
}

At each stage around the loop, we're going to add the contents of the variable temp to the variable input, and then read another line:

while (temp != null) { 
input = input + temp;
temp = in.readLine();
}

And then finally, we close the file:

in.close(); 

And in this way we read all of the contents of the text file into our input variable. What we do with it after that is up to us!

Although the code for these two techniques is very simple, we've exponentially increased the power of the applications we are able to write. Even relatively sophisticated applications like text editors are now within our grasp. Just consider the code for a simple implementation of such a thing:

import javax.swing.*; 
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;

class WritingFiles extends JFrame implements ActionListener {
JButton myButton, myButton2;
JTextArea myText;

public WritingFiles() {
myButton = new JButton ("Write To File");
myButton.addActionListener (this);
myButton2 = new JButton ("Load from File");
myButton1.addActionListener (this);
add (myButton, BorderLayout.NORTH);
myText = new JTextArea (10, 10);
add (myText, BorderLayout.CENTER);
add (myButton2, BorderLayout.SOUTH);
}


public static void main (String args[]) {
WritingFiles mainFrame = new WritingFiles();
mainFrame.setSize (400, 400);
mainFrame.setTitle ("WritingFiles");
mainFrame.setVisible (true);
}


public void actionPerformed (ActionEvent e) {
FileWriter base = null;
PrintWriter out = null;
FileReader readBase = null;
BufferedReader in = null;
BufferedWriter bout = null;
String temp = null;
if (e.getSource() == myButton) {
try {
base = new FileWriter ("temp.txt");
}

catch (IOException except) {
JOptionPane.showMessageDialog (null, "A Horrible Error!");
}

bout = new BufferedWriter (base);
out = new PrintWriter (bout);
out.println (myText.getText());
out.close();
}

else {
try {
readBase = new FileReader ("temp.txt");
in = new BufferedReader (readBase);
temp = in.readLine();
myText.setText ("");
while (temp != null) {
myText.append (temp + "\n");
temp = in.readLine();
}

in.close();
}

catch (Exception except) {
JOptionPane.showMessageDialog (null, "A Horrible Error!");
}

}

}

}

This very simple application allows us to write text into a JTextArea, save it, and then later load it back... all the mechanisms we need for a basic word processor!

It may not seem like much right now, but as was the case with learning to use arrays, file IO allows for much more complex and useful programs to be written. Now we are genuinely moving towards being competent programmers who can solve a range of interesting problems through the power of our voodoo coding fingers... Exciting, I'm sure you'll agree!

We're not done yet, of course. There's more... oh, so much more. Come follow the trail of breadcrumbs to our gingerbread house of file efficiency.

19.5

File I/O Efficiency

Above we made a point of making use of BufferedReader and BufferedWriter to improve the efficiency of our file IO operations. The truth is, like many of the simple efficiency improvements, it's really only an issue when large amounts of file IO is being performed. If you're dumping the contents of a text-field into a file, then who is going to notice if your method takes ten milliseconds as opposed to five?

If we're doing a large amount of file IO, the difference can be very noticeable. In this section we're going to look at some figures that can help illustrate the kind of benefits that buffering can bring.

Let's consider a variation of the simple timeMethod architecture from chapter three, and the simple setup for file IO discussed above. First we'll try without the BufferedWriter, and time how long 2,000,000 write operations take - we don't need to use nanoTime for this as the standard getTime method in Date offers more than enough granularity:


public void actionPerformed (ActionEvent e) {
Date time = new Date();
long now;
long then;
if (e.getSource() == myButton) {
try {
base = new FileWriter ("temp.txt");
out = new PrintWriter (base);
now = time.getTime();
for (int i = 0; i < 200000; i++) {
out.write (myText.getText());
}

time = new Date();
then = time.getTime();
System.out.println ("Writing took "+ (then - now) + " milliseconds.");
out.close();
}

}

}

Let's see what values we get for some choice phrases:

Unbuffered File Efficiency
Fig 19.2: Unbuffered File Efficiency

Now we try it with a BufferedWriter... the framework is exactly the same (and we'll use exactly the same phrases)... we only change the setup of the PrintWriter object:

Date time = new Date(); 
long now;
long then;
if (e.getSource() == myButton) {
try {
base = new FileWriter ("temp.txt");
bout = new BufferedWriter (base);
out = new PrintWriter (bout);
now = time.getTime();
for (int i = 0; i < 200000; i++) {
out.write (myText.getText());
}

time = new Date();
then = time.getTime();
System.out.println ("Writing took "+ (then - now) + " milliseconds.");
out.close();
}

Note that the setup of the BufferedWriter occurs outside of the actual timing code - this is important, as setting up an object (or not setting up an object) within the code we are efficiency testing will skew the results. Once we've run thse tests, we can fill in the values for buffered writing:

Buffered File Efficiency
Fig 19.3: Buffered File Efficiency

You might think that the benefits provided by the BufferedWriter are of sufficiently slim margins that it doesn't matter... however, considering that it adds no real difficulty to the setup of a writer object, it's almost a zero-cost addition. Also, the more writing you are doing, the bigger the benefit.

What is it that causes the performance disparity? It comes down to the way the two objects handle write requests. FileWriters write to the file as soon as they are told to via the write method. Hard-drive access is always slower than memory access, and so each write operation is more costly.

A BufferedWriter will maintain a memory buffer of things that are to be written, and only write to the hard-drive when that buffer is full. The FileReader and BufferedReader objects operate on the same principles, except for reading.

Java tip

One of the big bottlenecks in performance comes from File IO - limit this as much as possible, and make use of all the efficiency savings you can when developing the routines. The general rule about premature optimisation being a dangerous route is still valid, but you will often find fine-tuning file IO to be one of the richest veins for performance enhancment.


The net result is that buffering leads to more efficient file IO, and the more IO you are performing, the bigger the benefit will be.

19.6

JFileChooser

So far we've been basing the creation of our FileReader and FileWriter objects on a string containing a filename that we type into our program. There are legitimate reasons for writing a piece of code that dumps text into a predefined text-file... an application that makes use of internal logging, for example.

In addition to this however we need to allow the user a way to specify a filename. You wouldn't be happy if you bought a word processor that decided everything you wrote was going to be saved into a file called bing.txt... so why should anyone using your applications be treated to an inferior user experience?

The first and most obvious way to allow a user to specify a filename is to simply allow them to type it into a JTextField, and then when you create the FileReader/FileWriter, you can take the information from the text field and use it as a parameter to the constructor:

String file = myText.getText(); 
FileWriter myWriter = new FileWriter (file);

This adds a little extra complexity in that you need to ensure filenames are valid and that the user has actually typed something. The FileWriter will create a file if it needs to, but it won't create elaborate directory structures to ensure the file path is correct... it will instead throw an exception.

Even assuming all the consistency handling code is correct, this is still a far from desirable solution. After all, most users have a file system that is at least moderately complex, and if they have a directory for their files stored somewhere within the bowels of a complicated directory chain, it's just begging for a transcription error if they have to type the whole thing into your program:

C:\documents\michael\my files\text files\example files\java generated\2004\

This is especially problematic if you are on an unfamiliar system where you don't know the directory structure. Forcing the user to type in a filename is a quick and ugly hack to solve the problem, and is not worthy of fine developers such as ourselves.

Most applications have a somewhat more elegant solution - simply allow the user to browse to a directory and choose a file. Our Java programs should do the same. Luckily we don't need to code this functionality - Java comes with a class called JFileChooser that lets us provide this interface in a consistent and useful way.

We need to create an instance of the JFileChooser class in order to make use of it. Its constructor takes no parameters:

JFileChooser myChooser = new JFileChooser(); 

We can then make use of its very useful methods. The two most common of these are:

  • showSaveDialog, which shows a dialog tailored to saving a file.
  • showOpenDialog, which shows a dialog tailored to opening a file.

These methods do almost the same thing, except that the dialog box that appears will have 'save' or 'open' text in the appropriate places depending on which one we invoked.

Both of these methods take a parameter that indicates where we want the dialog to be displayed. We will ignore this and simply use this as a parameter:

myChooser.showSaveDialog (this); 

When this line of code is executed, your Java application will display a save dialog box much like the ones you would be familiar with from other applications:

JFileChooser Save dialog
Fig 19.4: JFileChooser Save dialog

The user then navigates to whatever directory they are looking for, and either double -clicks on a file, or types in a new file name. The chooser dialog deals with all of this complicated functionality - we don't need to code any of it.

The other method shows exactly the same kind of dialog box, but with the text slightly changed:

JFileChooser Open dialog
Fig 19.5: JFileChooser Open dialog

Functionally they are almost identical, providing a clean and consistent interface for navigating a particular system's directory structure.

However, we as developers must ensure that what happens after the user is finished with this dialog box is correct. If the user presses cancel, we don't want to continue on with the rest of our file IO code, for example. We must check the return value of our showOpenDialog/showSaveDialog method to see what button they pressed. This is very similar in form to what we do for the JOptionPane class.

Java tip

Never force the user to type in a filename - always provide a JFileChoser. It just seems much more professional, and is a huge aid to usability.


There is a method in the chooser class called getSelectedFile, which returns an object of type File (more on this next chapter). The File object has a method called getAbsolutePath which returns a string representing which directory the user navigated to, and what filename they chose. We can use this as the basis of our file IO objects.

The code for doing all of this is as follows:

String filename; 
File tempFile;
if (myChooser.showSaveDialog (this) ==JFileChooser.APPROVE_OPTION) {
tempFile = myChooser.getSelectedFile();
filename = tempFile.getAbsolutePath() }

By checking against which button the user pressed, we can ensure that our application behaves correctly and also provides a useful interface for anyone unlucky enough to be confronted with our code.

Consider our simple text editor from earlier - it saves the information to a text file that we (the developers) specify. It's an ideal candidate for modifying with our new JFileChooser knowledge, so let's do that! The only method that needs to change is actionPerformed, which needs to be expanded to deal with the new functionality.

The interface to the application is identical - it is only when someone decides that they want to open or save a file that the chooser is displayed. First, we'll update the reading code:

myChooser = new JFileChooser(); 
if (e.getSource() == myButton) {
if (myChooser.showSaveDialog (this) == JFileChooser.APPROVE_OPTION)
{
try {
tempFile = myChooser.getSelectedFile();
filename = tempFile.getAbsolutePath();
base = new FileWriter (filename);
}

catch (IOException except) {
JOptionPane.showMessageDialog (null, "A Horrible Error!");
}

bout = new BufferedReader (base);
out = new PrintWriter (bout);
out.println (myText.getText());
out.close();
}

}

And then we do the same thing for writing a file:

if (myChooser.showOpenDialog (this) == JFileChooser.APPROVE_OPTION) { 
try {
tempFile = myChooser.getSelectedFile();
filename = tempFile.getAbsolutePath();
readBase = new FileReader (filename);
in = new BufferedReader (readBase);
temp = in.readLine();
myText.setText ("");
while (temp != null) {
myText.append (temp + "\n");
temp = in.readLine();
}

in.close();
}

catch (Exception except) {
JOptionPane.showMessageDialog (null, "A Horrible Error!");
}

}

19.7

File Filters

One of the mechanisms provided by file dialogs is the restriction of the list presented to only those items with which we are interested. If I have an application that can read only text files, I'm only going to want those files that I can actually hope to open. Usually in such cases the dialog restricts the list of files we see to only those with specific extensions.

We can do this in Java also through the use of a FileFilter class... alas, it's not quite so simple as just indicating which extensions we wish to allow. We must provide a special class that parses file-names for suitability.

The class we write extends from a class called FileFilter (found in javax.swing.filechooser.*)... we need to implement two methods before the class will compile.

The first is the one the manages which files to list, and it's called accept. It returns a boolean parameter, and takes a File object as a parameter. We analyse this file object (we'll see more methods for this in the next chapter) and return true if we want to display it in the list of files for the dialog, and false if we don't:

The second is a textual description of the filter... this is just a String returned from the method.

There is a small problem... there are actually two FileFilter classes in the Java libraries. One of these is found in java.io, and the other (the one we want) is found in javax.swing.filechooser. Unfortunately, we need to import both packages for the FileFilter, because we also need access to the File class. This causes Java to pani.. you've said you want a FileFilter, but it has two of them! Which one should it use?

We can solve the problem in two ways... one, we can limit what we import to only the classes we are interested in. The * symbol in the import statement tells Java that we want all of the classes. We can specify an exact class:

import java.io.File; 

Or, we can disambiguiate the class for Java when we refer to it... rather than giving just the class name, we give its full qualified name:


public class myClass extends javax.swing.filechooser.FileFilter {
}

The latter requires us to disambiguiate every single reference to the FileFilter class, so we'll stick with importing just the class we need for our example.

A simple implementation of the FileFilter class gives us something approaching the following:

import javax.swing.filechooser.*; 
import java.io.File;


public class FilterClass extends FileFilter {

public boolean accept (File tmp) {
String path = tmp.getAbsolutePath();
if (path.endsWith (".txt")) {
return true;
}

return false;
}


public String getDescription() {
return "Text Files";
}

}

We then set the filter on the JFileChooser object through the use of the setFileFilter method:

myChooser = new JFileChooser(); 
myFilter = new FilterClass();
myChooser.setFileFilter (myFilter);

Unfortunately, when we run our newly filtered dialog, we see a problem... it filters out everything that doesn't end with .txt... including directories. That means we can't actually navigate to where our files may be found. There's a method in File called isDirectory that returns true or false... we can use that to make sure we can navigate to the directory we need. We'll talk about other methods in File in the next chapter:

import javax.swing.filechooser.*; 
import java.io.File;


public class FilterClass extends FileFilter {

public boolean accept (File tmp) {
String path = tmp.getAbsolutePath();
if (tmp.isDirectory() == true) {
return true;
}

if (path.endsWith (".txt")) {
return true;
}

return false;
}


public String getDescription() {
return "Text Files";
}

}

And now when we apply the filter to our chooser, we get only those files in which we are interested, as well the directories we need to be able to navigate through the drive. Huzzah for us!

Sometimes we want to provide a range of FileFilters... for example, we may also want one that allows for the search to be restricted to less likely, but still valid, file formats. If we can parse .txt files, perhaps we can also parse .log files... we can add a second (and third, and fourth, and...) filter to the JFileChooser through the addChoosableFileFilter method. We add as many filters as we desire, and the user can select between them as desired:

import javax.swing.filechooser.*; 
import java.io.File;


public class LogFilter extends FileFilter {

public boolean accept (File tmp) {
String path = tmp.getAbsolutePath();
if (tmp.isDirectory() == true) {
return true;
}

if (path.endsWith (".log")) {
return true;
}

return false;
}

public String getDescription() {
return "Log Files";
}

}

And then:

myChooser.addChoosableFileFilter (new FilterClass()); 
myChooser.addChoosableFileFilter (new LogFilter());

In this way, we can provide as much filtering as we desire.

Java tip

If you do provide File Filters, always provide one that allows for the user to view all files. Your application may not be able to read them, but you should allow for the user to choose their own weird naming conventions providing the file format is correct.


19.8

An Aside about File Parsing

We haven't looked at file parsing at all in this chapter, and we won't be looking at it in the next chapter either. We're left with the predicament that we know how to read in a file, but not what to do with it when we have.

If it's a simple text file, then we can store it in a string. That's pretty straightforward.

Often the data is stored in a more complex format and requires some degree of manipulation before useful information can be extracted.

There are no base classes for parsing text data in Java. Actually, that's not quite true - there are standards like XML that have considerable internal and external support. Unless the data is stored in a standard format like this (and we won't be looking at that in this book), we must deal with the file parsing ourselves.

This is the difficult part of File IO - the techniques for reading and writing files are part of a toolkit that you are developing as you work through this book. Actually doing something with those tools is the difficult part. Within your toolkit, however you do possess many tools that can be applied to file parsing. Consider for example a text file that lists a number of student grades:

1, 2, 5, 1, 4, 2, 7, 8, 9, 4, 3, 2, 1

Using the techniques we have discussed in this chapter, you can read this list of numbers into a string. Then comes the difficult part of doing something with the string - but wait! We talked about String Tokenization in an earlier chapter also... we can use that to break up this string into individual elements that contain each of the grades. From that point on, it's easy to do whatever you want with the data.

Java tip

The skills you have developed regarding standard String parsing can be usefully applied to the parsing of standard stream based files. In principle they are exactly the same.


19.9

Conclusion

File Parsing is very much application specific. There is no 'quick and easy' fix that will always work. However, the good news is that once you have done the work of reading a file from disk, it's just a case of parsing strings from that point onwards - and we already know how to do that!

Further Reading

The following table details further reading on the topic in this chapter, and also any external resources that you may find useful.

ResourceDescription
Example Programs from this chapterThis is a zip file of all the programs shown in this chapter.

PreviousTable of ContentsNext

© 2004-2006 Michael James Heron