|
|
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 2 of 2
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:
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:
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.
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.
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.)
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:
runFinalizersOnExit(true) in class Runtime or SystemObject, always invoke super.finalize() at the end of your finalizers
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.
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.