Object finalization and cleanup

How to design classes for proper object cleanup

1 2 Page 2
Page 2 of 2

Managing non-memory resources

Because heap memory is automatically reclaimed by the garbage collector, the main thing you need to worry about when you design an object's end-of-lifetime behavior is to ensure that finite non-memory resources, such as file handles or sockets, are released. You can take any of three basic approaches when you design an object that needs to use a finite non-memory resource:

  1. Obtain and release the resource within each method that needs the resource
  2. Provide a method that obtains the resource and another that releases it
  3. Obtain the resource at creation time and provide a method that releases it

Approach 1: Obtain and release within each relevant method

As a general rule, the releasing of non-memory finite resources should be done as soon as possible after their use because the resources are, by definition, finite. If possible, you should try to obtain a resource, use it, then release it all within the method that needs the resource.

A log file class: An example of Approach 1

An example of a class where Approach 1 might make sense is a log file class. Such a class takes care of formatting and writing log messages to a file. The name of the log file is passed to the object as it is instantiated. To write a message to the log file, a client invokes a method in the log file class, passing the message as a String. Here's an example:

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.IOException;
class LogFile {
    private String fileName;
    LogFile(String fileName) {
        this.fileName = fileName;
    }
    // The writeToFile() method will catch any IOException
    // so that clients aren't forced to catch IOException
    // everywhere they write to the log file.  For now,
    // just fail silently. In the future, could put
    // up an informative non-modal dialog box that indicates
    // a logging error occurred. - bv 4/15/98
    void writeToFile(String message) {
        FileOutputStream fos = null;
        PrintWriter pw = null;
        try {
            fos = new FileOutputStream(fileName, true);
            try {
                pw = new PrintWriter(fos, false);
                pw.println("------------------");
                pw.println(message);
                pw.println();
            }
            finally {
                if (pw != null) {
                    pw.close();
                }
            }
        }
        catch (IOException e) {
        }
        finally {
            if (fos != null) {
                try {
                    fos.close();
                }
                catch (IOException e) {
                }
            }
        }
    }
}

Class LogFile is a simple example of Approach 1. A more production-ready LogFile class might do things such as:

  • Insert the date and time each log message was written

  • Allow messages to be assigned a level of importance (such as ERROR, INFO, or DEBUG) and enable a level to be set that will prevent unwanted detail (such as DEBUG messages) from making it into the log file
  • Manage in some way the size of the log file, i.e., by copying it to a different filename and starting fresh each time the log file achieves a certain size

The main feature of this simple version of class LogFile is that it surrounds each log message with a series of dashes and a blank line.

Using finally to ensure resource release

Note that in the writeToFile() method, the releasing of the resource is done in finally clauses. This is to make sure the finite resource (file handle) is actually released no matter how the code is exited. If an IOException is thrown, the file will be closed.

Pros and cons of Approach 1

The approach to resource management taken by class LogFile (Approach 1 from the above list) helps make your class easy to use, because client programmers don't have to worry about explicitly obtaining or releasing the resource. In both Approach 2 and 3 from the list above client programmers must remember to explicitly invoke a method to release the resource. In addition -- and what can be far more difficult -- client programmers must figure out when their programs no longer need a resource.

A problem with Approach 1 is that obtaining and releasing the resource each time you need it may be too inefficient. Another problem is that, in some situations, you may need to hold onto the resource between invocations of methods that use the resource (such as writeToFile()), so no other object can have access to it. In such cases, one of the other two approaches is preferable.

Approach 2: Offer methods for obtaining and releasing resources

In Approach 2 from the list above, you provide one method for obtaining the resource and another method for releasing it. This approach enables the same class instance to obtain and release a resource multiple times. Here's an example:

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.IOException;
class LogFileManager {
    private FileOutputStream fos;
    private PrintWriter pw;
    private boolean logFileOpen = false;
    LogFileManager() {
    }
    LogFileManager(String fileName) throws IOException {
        openLogFile(fileName);
    }
    void openLogFile(String fileName) throws IOException {
        if (!logFileOpen) {
            try {
                fos = new FileOutputStream(fileName, true);
                pw = new PrintWriter(fos, false);
                logFileOpen = true;
            }
            catch (IOException e) {
                if (pw != null) {
                    pw.close();
                    pw = null;
                }
                if (fos != null) {
                    fos.close();
                    fos = null;
                }
                throw e;
            }
        }
    }
    void closeLogFile() throws IOException {
        if (logFileOpen) {
            pw.close();
            pw = null;
            fos.close();
            fos = null;
            logFileOpen = false;
        }
    }
    boolean isOpen() {
        return logFileOpen;
    }
    void writeToFile(String message) throws IOException {
        pw.println("------------------");
        pw.println(message);
        pw.println();
    }
    protected void finalize() throws Throwable {
        if (logFileOpen) {
            try {
                closeLogFile();
            }
            finally {
                super.finalize();
            }
        }
    }
}

In this example, class LogFileManager declares methods openLogFile() and closeLogFile(). Given this design, you could write to multiple log files with one instance of this class. This design also allows a client to monopolize the resource for as long as it wants. A client can write several consecutive messages to the log file without fear that another thread or process will slip in any intervening messages. Once a client successfully opens a log file with openLogFile(), that log file belongs exclusively to that client until the client invokes closeLogFile().

Note that LogFileManager uses a finalizer as a fallback in case a client forgets to invoke closeLogFile(). As mentioned earlier in this article, this is one of the more common uses of finalizers.

Note also that after invoking closeLogFile(), LogFileManager's finalizer invokes super.finalize(). Invoking superclass finalizers is good practice in any finalizer, even in cases (such as this) where no superclass exists other than Object. The JVM does not automatically invoke superclass finalizers, so you must do so explicitly. If someone ever inserts a class that declares a finalizer between LogFileManager and Object in the inheritance hierarchy, the new object's finalizer will already be invoked by LogFileManager's existing finalizer.

Making super.finalize() the last action of a finalizer ensures that subclasses will be finalized before superclasses. Although in most cases the placement of super.finalize() won't matter, in some rare cases, a subclass finalizer may require that its superclass be as yet unfinalized. So, as a general rule of thumb, place super.finalize() last.

Approach 3: Claim resource on creation, offer method for release

In the last approach, Approach 3 from the above list, the object obtains the resource upon creation and declares a method that releases the resource. Here's an example:

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.IOException;
class LogFileTransaction {
    private FileOutputStream fos;
    private PrintWriter pw;
    private boolean logFileOpen = false;
    LogFileTransaction(String fileName) throws IOException {
        try {
            fos = new FileOutputStream(fileName, true);
            pw = new PrintWriter(fos, false);
            logFileOpen = true;
        }
        catch (IOException e) {
            if (pw != null) {
                pw.close();
                pw = null;
            }
            if (fos != null) {
                fos.close();
                fos = null;
            }
            throw e;
        }
    }
    void closeLogFile() throws IOException {
        if (logFileOpen) {
            pw.close();
            pw = null;
            fos.close();
            fos = null;
            logFileOpen = false;
        }
    }
    boolean isOpen() {
        return logFileOpen;
    }
    void writeToFile(String message) throws IOException {
        pw.println("------------------");
        pw.println(message);
        pw.println();
    }
    protected void finalize() throws Throwable {
        if (logFileOpen) {
            try {
                closeLogFile();
            }
            finally {
                super.finalize();
            }
        }
    }
}

This class is called LogFileTransaction because every time a client wants to write a chunk of messages to the log file (and then let others use that log file), it must create a new LogFileTransaction. Thus, this class models one transaction between the client and the log file.

One interesting thing to note about Approach 3 is that this is the approach used by the FileOutputStream and PrintWriter classes used by all three example log file classes. In fact, if you look through the java.io package, you'll find that almost all of the java.io classes that deal with file handles use Approach 3. (The two exceptions are PipedReader and PipedWriter, which use Approach 2.)

Conclusion

The most important point to take away from this article is that if a Java object needs to take some action at the end of its life, no automatic way exists in Java that will guarantee that action is taken in a timely manner. You can't rely on finalizers to take the action, at least not in a timely way. You will need to provide a method that performs the action and encourage client programmers to invoke the method when the object is no longer needed.

This article contained several guidelines that pertain to finalizers:

  • Don't design your Java programs such that correctness depends on "timely" finalization

  • Don't assume that a finalizer will be run by any particular thread

  • Don't assume that finalizers will be run in any particular order

  • Avoid designs that require finalizers to resurrect objects; if you must use resurrection, prefer cloning over straight resurrection

  • Remember that exceptions thrown by finalizers are ignored

  • If your program includes objects with finalizers that absolutely must be run before the program exits, invoke runFinalizersOnExit(true) in class Runtime or System

  • Unless you are writing the finalizer for class Object, always invoke super.finalize() at the end of your finalizers

Next month

In next month's Design Techniques I'll continue the mini-series of articles that focus on designing classes and objects. Next month's article, the fifth of this mini-series, will discuss when to use -- and when not to use -- exceptions.

A request for reader participation

Software design is subjective. Your idea of a well-designed program may be your colleague's maintenance nightmare. In light of this fact, I am trying to make this column as interactive as possible.

I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know.

Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill.

Learn more about this topic

1 2 Page 2
Page 2 of 2