Bruce Eckel’s Thinking in Java | Contents | Prev | Next |
writing code with exceptions, it’s particularly important that you always
ask, “If an exception occurs, will this be properly cleaned up?”
Most of the time you’re fairly safe, but in constructors there’s a
problem. The constructor puts the object into a safe starting state, but it
might perform some operation – such as opening a file – that
doesn’t get cleaned up until the user is finished with the object and
calls a special cleanup method. If you throw an exception from inside a
constructor, these cleanup behaviors might not occur properly. This means that
you must be especially diligent while you write your constructor.
you’ve just learned about finally,
you might think that it is the correct solution. But it’s not quite that
simple, because
finally
performs
the cleanup code
every
time
,
even in the situations in which you don’t want the cleanup code executed
until the cleanup method runs. Thus, if you do perform cleanup in
finally,
you must set some kind of flag when the constructor finishes normally and
don’t do anything in the finally block if the flag is set. Because this
isn’t particularly elegant (you are coupling your code from one place to
another), it’s best if you try to avoid performing this kind of cleanup in
finally
unless you are forced to.
the following example, a class called
InputFile
is created that opens a file and allows you to read it one line (converted into
a
String)
at a time. It uses the classes FileReader
and BufferedReader
from the Java standard IO library that will be discussed in Chapter 10, but
which are simple enough that you probably won’t have any trouble
understanding their basic use:
//: Cleanup.java // Paying attention to exceptions // in constructors import java.io.*; class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Other code that might throw exceptions } catch(FileNotFoundException e) { System.out.println( "Could not open " + fname); // Wasn't open, so don't close it throw e; } catch(Exception e) { // All other exceptions must close it try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } throw e; } finally { // Don't close it here!!! } } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.out.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } } } public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.out.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(); } } } ///:~
constructor for
InputFile
takes a
String
argument, which is the name of the file you want to open. Inside a
try
block, it creates a
FileReader
using the file name. A
FileReader
isn’t particularly useful until you turn around and use it to create a
BufferedReader
that you can actually talk to – notice that one of the benefits of
InputFile
is that it combines these two actions.
the
FileReader
constructor is unsuccessful, it throws a FileNotFoundException,
which must be caught separately because that’s the one case in which you
don’t want to close the file since it wasn’t successfully opened.
Any
other
catch clauses must close the file because it
was
opened by the time those catch clauses are entered. (Of course, this is
trickier if more than one method can throw a
FileNotFoundException.
In that case, you might want to break things into several
try
blocks.)
The
close( )
method throws an exception that is tried and caught even though it’s
within the block of another
catch
clause – it’s just another pair of curly braces to the Java
compiler. After performing local operations, the exception is re-thrown, which
is appropriate because this constructor failed, and you wouldn’t want the
calling method to assume that the object had been properly created and was valid.
this example, which doesn’t use the aforementioned flagging technique, the
finally
clause is definitely
not
the place to
close( )
the file, since that would close it every time the constructor completed. Since
we want the file to be open for the useful lifetime of the
InputFile
object this would not be appropriate.
getLine( )
method returns a
String
containing the next line in the file. It calls readLine( ),
which can throw an exception, but that exception is caught so
getLine( )
doesn’t throw any exceptions. One of the design issues with exceptions
is whether to handle an exception completely at this level, to handle it
partially and pass the same exception (or a different one) on, or whether to
simply pass it on. Passing it on, when appropriate, can certainly simplify
coding. The
getLine( )
method becomes:
String getLine() throws IOException { return in.readLine(); }
of course, the caller is now responsible for handling any
IOException
that might arise.
cleanup( )
method must be called by the user when they are finished using the
InputFile
object to release the system resources (such as file handles) that are used by
the
BufferedReader
and/or
FileReader
objects.
[46]
You don’t want to do this until you’re finished with the
InputFile
object, at the point you’re going to let it go. You might think of
putting such functionality into a finalize( )
method, but as mentioned in Chapter 4 you can’t always be sure that
finalize( )
will be called (even if you
can
be sure that it will be called, you don’t know
when).
This is one of the downsides to Java – all cleanup other than memory
cleanup doesn’t happen automatically, so you must inform the client
programmer that they are responsible, and possibly guarantee that cleanup
occurs using
finalize( ).
Cleanup.java
an
InputFile
is created to open the same source file that creates the program, and this file
is read in a line at a time, and line numbers are added. All exceptions are
caught generically in
main( ),
although you could choose greater granularity.
of the benefits of this example is to show you why exceptions are introduced at
this point in the book. Exceptions are so integral to programming in Java,
especially because the compiler enforces them, that you can accomplish only so
much without knowing how to work with them.