Sunteți pe pagina 1din 37

Abstract

Modern object oriented programming languages like Java and C# offer advanced
exception
handling mechanisms. As systems grow more complex and customer demand for
robustness
increases, understanding and knowing how to use exception handling techniques
effectively is
increasingly important. In the past, developers considered exceptions as an
afterthought or an
as-you-go addition, resulting in difficult debugging and unstable programs. Working
on this
project, we searched books, articles and the web for guidelines and pitfalls
regarding the use
of Java exceptions. We tested integrated development environments and other
software
designed to help analyze Java programs and the use of exceptions. Based on what we
have
learned, this paper formulates a set of rules to help system developers create
robust Java
programs with the use of exceptions. To build a robust system, the developers need
to take
exception handling into account throughout the development process. Exception
handling is
an integral part of a robust system, and can not be added as an afterthought.
Java’s exception
handling mechanisms are powerful, easy to use, and easy to misuse. This paper
proposes
some rules to help avoid or correct the misuses of exceptions.

Preface
This document is the result of our work in the course TDT4735 Systemutvikling,
Fordypning.
The work has been performed at the Department of Computer and Information Science,
Faculty of Information Technology, Mathematics and Electrical Engineering at the
Norwegian University of Science and Technology (NTNU) during the autumn semester of
2003.

1 Introduction
Exception handling is something most people have heard about, but do not always
know how
to use properly. From our own experience and others, exception handling was the
last
mechanism we learnt and the last to be used. We will with this report show the
importance
and necessity of exception handling.
1.1 Motivation and Background
We got the idea for this report after a meeting with our teaching supervisor, Tor
Stålhane. He
made us aware of the lack of guidance, guidelines, rules and tips within exception
handling in
Java. Java is a programming language that we have been familiar with since the
freshman
year. We have been programming with Java ever since, but did not realize how
important
exception handling was until recently.
When we reflect on our work with exception handling, we can see the lack of
understanding
and use of exception handling. The literature which introduced us to Java only gave
us a little
knowledge about exception handling. After some research, we noticed how few
guidelines
that was available for exception handling. We saw at once the potential for writing
a complete
guideline or a set of rules for exception handling.
With powerful development tools and increasing hardware performance, systems tend
to
become more and more complex in order to take advantage of these opportunities.
Complex
systems are harder to code, harder to debug and harder to maintain. The more
complex the
systems get the more unstable they will become. With the increasing demand for
robust
system from the customers, developers are forced to focus more on reliability and
robustness.
Exception code design and analysis is complex and can be hard to handle. The
different
components have to co-operate and interact with each other in faultless way. The
process of
exception handling is not straight forward but rather complex. When one component
decides
to throw an exception it can not handle, that exception can be caught anywhere in
the
program. Code violation can occur anywhere in the code and have to be handled.
Developers
have to plan in advance how exceptions are handled before they occur. Logging for
failure
that occurred also has to be considered thoroughly.
It is not easy to known where or when to include exception handling in your
development
process. Exception handling is usually not addressed at the appropriate phase of
system
development. People tend to forget about exception handling and only take them in
consideration when they encounter problems with their program. This will only cause
the
program to be more unstable and harder to debug.
Even though the developers do plan exception handling ahead, they do not always
know how
to use them properly. Methodologies supporting proper use of exception handling are
few and
far between. The methodologies that are available today are not complete, they are
rather
scattered. You may find many books that include many good guides for using
exceptions, but
you may find other guides elsewhere not mentioned in the books.
2
1.2 Problem Description
Many developers and programmers do not know how to use exceptions efficiently. Some
see
Java exceptions as a hassle, and only think about them when forced to do so by the
Java
compiler. The exceptions are inserted into the system too late, and are often
caught and just
ignored to stop the compiler from complaining. Other developers see the need for
increasing
the robustness of their program after it has been implemented, and try to plug
exception
handling into the system near the end of the process, usually much too late.
We want to show that Java’s exception handling mechanism is a powerful and
essential tool
for creating robust programs. We will present a rule set to help developers use
Java
exceptions for more than just showing error messages and stack traces. We will show
the
importance of using these rules throughout the development process. Starting with
the system
requirements phase, we will suggest rules to help developers use exceptions in a
better way
through the different phases of architectural design, detailed design,
implementation and
testing and delivery of the finished product.
1.3 Related Work
Although there is an overabundance of books about Java, and many of these include a
chapter
about how Java’s exception handling mechanisms work, there are only a few books
that delve
deeper into the subject of good use of exceptions. [PRJ2000] is a collection of
practical
suggestions, advice, examples and discussions about programming in the Java
language. It has
a section for exception handling that contains eleven good practices when using
exceptions
and the reasoning behind them. [EFJ2001] is a guide to effective use of the Java
programming
language. Its chapter about exceptions contains nine rules for using exceptions
effectively, to
improve a program’s readability, reliability, and maintainability.
Of the publicly available articles about exception handling, many had a narrow
focus on some
particular aspect of exception handling, and thus did not help us much in our
search for
general guidelines and best practices. One article we think is a good read is
[EAE2003]. It
presents a simple component–based strategy addressing the various ways for using
exceptions
better in Java. The paper is based on the experience of the development of many
real large
software projects. Another article which we also find useful and interesting in
relation to our
work is [AEU2003]. This article discusses common trends in the use of exception
handling in
large Java applications, and proposes some solutions to the more common pitfalls.
[EXS2003]
compares Java exceptions with C# exceptions, and also presents a dozen strategies
for using
exceptions in Java.
The World Wide Web is a great resource for information, and there are plenty of
forums
where you can get hints and tips and read about exception handling in Java. Many of
the sites
and articles on the net have more guidelines than the books you may find in your
local store.
3
1.4 Structure of Report
We start this paper by giving the reader a basic introduction to exception
handling. Chapter 2
describes what exception handling is and why programming languages have this
mechanism,
in addition to a short history of exception handling. Chapter 3 describes Java’s
exception
handling mechanisms in detail. Chapter 4 looks at current trends in exception
handling and
summarizes information that is currently available. Several tips and guidelines are
gathered
from various books, articles and forums. We have tested several programs and
toolkits
concerning exception handling in Java in Chapter 5. Our main contribution is in
Chapter 6
where we present a set of rules to help developers create more robust programs. We
then look
at the rules in the context of real life examples in Chapter 7, discussing what is
good, what is
bad and what could have been done better. Chapter 8 concludes the report, and looks
at
possible further work.
4
2 Exception Handling
Following is a short introduction to exception handling. Sections 2.1 and 2.2
explain why
programming languages have exceptions and how exception handling works. We will
then
provide the user with a short history of exception handling in section 2.3.
2.1 What they are for
The main idea behind exception handling is to make programs more reliable and
robust. Many
programming languages provide exception handling mechanisms to allow software
developers to define exceptional conditions. This helps the developers to structure
the
exceptional activities of the system component.
It is important to have a well thought-out and consistent exception handling
strategy for the
sake of efficiency and good programming practice. Exception handling should not be
considered as an add-on but as an integral part of the development process. The
power of
exceptions provides a framework on which to develop applications that are robust
and
dependable by design, rather than by accident.
2.2 How they work
There exist two types of activities taking place during the execution of a program;
normal and
abnormal activities. In the normal activities, software components may receive
service
requests and produce responses. If the component does not satisfy a service
request, it returns
an exception. There are several exceptions that can occur, as represented in Figure
2.1.We can
classify exceptions into three categories: interface, internal and failure.
- Interface exceptions: When a request does not conform to the component’s
interface
the response will be an interface exception.
- Internal exceptions: This is the type of exception that is called by the
component itself
in order to invoke its own internal exceptional handler.
- Failure exceptions: This occurs when the component itself is unable to handle
exceptions
If the component is able to handle the exception it will return to normal state and
the program
will continue to run normally. When the component is unable to handle the exception
it will
try to throw it back to the caller. If the caller is unable to handle the exception
it will try to
throw it to the next caller. This will go on until the exception is handled or else
the program
will halt.
5
Figure 2.1 Idealized fault-tolerant components taken from [EHM1999].
2.3 History
Already in the mid 1950’s we find something similar to exception handling, you may
call it a
predecessor to exception handling. It was the Lisp 1.5 developed by John McCarthy
who
introduced a function called “errset” which allowed the Lisp interpreter and
compiler to exit
when an error occurred. Later, in the mid 1960’s, a programming language, developed
at IBM
called PL/I, included facilities for dealing with control flow. The earlier
languages had few
facilities that dealt with exceptional conditions during execution. There were
several problems
with this mechanism, but the philosophy of programming language design for
reliability
demanded that this facility be included in the programming language definition. By
the mid
1970’s, software engineering and programming languages communities had a strong
concern
about software reliability.
In the Late 1970’s both Ada and CLU, inspired by John B. Goodenough’s paper
[EXH1975],
introduced another way to deal with exceptions. The exceptions were handled by the
caller
not where it was raised. Another feature was that the exception handling mechanisms
were
static instead of dynamic as PL/I was.
Treatment of the exceptions in programming languages in the 1980’s influenced the
software
engineering research on the design of programming development environments. In the
1990’s
when object-oriented programming was beginning to enter the world of software
development, exceptions were considered as objected. The try-catch method is most
commonly used in object-oriented programming today. For a more detailed reading see
[IDE2003].
Normal Activity
Abnormal Activity
(fault tolerance by exception
handling)
Service
requests
Normal
responses
Service
requests
Normal
responses
Return to normal
operation
Internal
exceptions
Interface
exceptions
Interface
exceptions
Failure
exceptions
Failure
exceptions
6
3 Java Exceptions
In this chapter we will look at how exceptions are implemented and used in the Java
language.
In Java, exceptions are first class citizens. This means that exceptions are
directly supported
by the language. Using Java Exceptions for what they are worth can greatly improve
the
robustness of a program. It helps distinguishing normal and abnormal behavior, and
separates
error-detection (throw) from error-handling (try-catch). In order to take advantage
of Java’s
flexible error-handling system, we need to understand how it works and some of the
design
decisions Sun did when making the exception handling mechanisms.
3.1 Exceptions as objects
“With exceptions as objects, you have the power to encapsulate an unlimited variety
of
functionality specific to the problem.”
- http://www.churchillobjects.com/c/11012k.html
Java exceptions are objects. This is a relatively new way to represent error
handling. C++ and
Java are the only well known languages today that have exceptions as objects. Older
languages use labels or have no real support for exceptional behavior. One example
is Virtual
Basic’s awkward ON ERROR GOTO functionality. Java exceptions are sometimes called
throwables because raising an exception in Java is done with the throw keyword.
Figure 3.1: Java API Exception Hierarchy
Throwable
(interface)
Exception Error
RuntimeException
ThreadDeath
VirtualMachineError
OutOfMemoryError
IOException
SQLException
NullpointerException
ArrayIndexOutOfBoundsException
7
Figure 3.1 shows the fundamentals of Java’s Common APIs exception hierarchy.
Inheriting
from the Throwable interface, the Exception and Error classes serve quite different
purposes.
While both signal an exceptional behaviour, the Error class and its subclasses are
used mainly
by the Java Virtual Machine to indicate severe (and often unrecoverable) errors.
The
Exception branch is more commonly used. Most notable is the RuntimeException class
with
subclasses like NullPointerException, ArrayIndexOutOfBoundsException and
ArithmeticException. These exceptions are usually the result of bad programming,
and are not
checked at compile time. See section 3.6 for more information about Runtime
Exceptions.
Exceptions that are not Runtime Exceptions are called Checked Exceptions (see
section 3.5).
When you use a method that throws a Checked Exception, you are required by the
compiler to
declare how the program is to respond, should the exception occur. Most checked
exceptions
are thrown by the programmer, and not by the virtual machine. These usually signal
an error
stemming from outside the program, for instance bad data by the user or failure to
communicate with a database or another machine.
3.2 The throw statement
When a situation occurs that the current class or method can not or will not
handle, an
exception is thrown. If the exception isn’t caught within the scope of the method,
it will be
thrown to the calling method. If this method doesn’t catch it either, it will be
thrown to the
next calling method – and so on, until the exception is caught or the program
terminates with
an error message. See Figure 3.2 for an illustration.
Figure 3.2: Two method calls, an exception and a catch statement
This system helps separate error detection from error handling. One method
discovers the
error and signals this by throwing an exception. This ends its responsibility
regarding the
situation. Any method in the chain of calls that is able to handle the exception
can do so, or
the user will be presented with an error message and a stack trace.
Main Method
Catch
Method 1
Method 2
Re-thrown
Call
Exception
Call
8
3.3 The try-catch construct
The try-catch statement is Java's way of separating normal code from error-handling
code.
When a programmer suspects that a block of code might generate an exception, it is
placed
inside a try block. This is followed by one or more catch statements, each
containing the code
to be executed if the matching exception is thrown. See Example 3.1.
Example 3.1 – The try-catch construct
try {
// code that might throw an exception
} catch (OneException e1) {
// code for handling OneException
} catch (AnotherException e2) {
// code for handling AnotherException
}
The catch statements are checked against the thrown exceptions in the order they
are declared.
3.4 Cleaning up with finally
Once an exception is thrown, the rest of the code in the try block is skipped. See
Example 3.2.
If an IOException is thrown in the third line, the file will not be closed in line
four.
Example 3.2 – Writing to a file without finally
1 try {
2 file.open();
3 file.write("something");
4 file.close();
5 } catch (IOException ioe) {
6 GUI.alertUser("Could not write to the file.");
7 }
A better way to handle this is with the finally statement. The code contained
within the finally
block will always run, whether any exceptions have been generated or not. See
Example 3.3.
The file will be closed in line 7 independent of what happens inside the try block.
Example 3.3 – Writing to a file with finally
1 try {
2 file.open();
3 file.write("something");
4 } catch (IOException ioe) {
5 GUI.alertUser("Could not write to the file.");
6 } finally {
7 file.close();
8 }
9
The only thing that can prevent a finally block from executing is a call to
System.exit, since
this will shut down the virtual machine.
3.5 Catch or declare
Unless a thrown exception is a subclass of RuntimeException, the Java compiler
requires that
all exceptions are caught or specifically thrown by the method. This means that if
you call a
method that throws an exception, you are required to declare how your program will
respond
if that exception occurs. Either you throw the exception further up the call stack,
or you catch
it and deal with it.
Example 3.4 – Catch or specify
/* Reads the contents of a textfile. If the file does not exist,
* creates an empty file and returns an empty string.
*/
public String readFile(String filename) throws IOException {
String fileContents = "";
FileReader filereader = null;
try {
filereader = new FileReader(filename);
} catch (FileNotFoundException notfound) {
new File(filename).createNewFile();
}
if (filereader != null) {
BufferedReader reader = new BufferedReader(filereader);
while (reader.ready()) {
fileContents += reader.readLine();
}
}
return fileContents;
}
Take a look at Example 3.4. The FileReader constructor can throw two checked
exceptions,
IOException and FileNotFoundException. One is caught and handled by the method. The
other, IOException, is specifically declared to be thrown by the method. Had this
not been
specified, it would result in a compiler error. Likewise, another method calling
this one will
have to catch the IOException or declare that it throws it further.
3.6 Runtime exceptions
As opposed to the exceptions in section 3.5, the ones inheriting from
RuntimeException are
not checked by the compiler. Runtime exceptions are problems detected by the
runtime
system. Common exceptions of this type are NullPointerException (trying to access
an object
through a null pointer reference), ArithmeticException (e.g. division by zero) and
ArrayIndexOutOfBounds (trying to access an element in a list with an index that is
either too
large or too small).
These exceptions can be numerous, possibly occurring anywhere in the code. For one,
this
makes it prohibitive to check the exceptions at compile time – and would require
that the
10
programmer catch these exceptions everywhere they could occur. It also would make
programmers spend all their time producing extremely cluttered code that did
nothing.
3.7 Chained exceptions
A new feature in Java Development Kit (JDK) 1.4.0 is known as chained exceptions.
In short,
exception chaining stores all exceptions from the initial cause up to the last.
Throwables can
now contain the throwable that caused them. One cause can have another cause, thus
giving a
causal chain. You can iterate through the chain, all the way back to the initial
exception.
Figure 3.3: A Chain of Exceptions
According to Sun, "it is common for Java code to catch one exception and throw
another"
[SUN1]. The problem with this is that the second exception will lose information
contained in
the first. This makes both bug finding and recovery attempts harder. When the
initial cause is
lost, the programmer must spend time trying to dig up what really happened. Chained
exceptions will let you know not only the initial cause, but also every exception
that was
triggered on the way up.
This knowledge can also be used effectively inside the program. Imagine a method
doing this:
Example 3.5 – Chaining exceptions
try {
// code
} catch (FileNotFoundException cause) {
// do something
throw new FileNotAvailableException(cause);
} catch (AccessDeniedException cause) {
// do something
throw new FileNotAvailableException(cause);
}
It is now possible to find out why the file was not available, without forcing the
calling
method to catch two separate exceptions. It might be used like this:
Example 3.6 – Using chained exceptions
try {
// code calling the method in Example 3.5.
} catch (FileNotAvailableException exception) {
// do something regardless of cause
if (exception.getCause() instanceof AccessDeniedException) {
// do something more
EXCEPTION EXCEPTION EXCEPTION
Initial cause
getCause() getCause()
11
}
}
It has always been possible to chain exceptions, either by making your own, or with
a wrapper
class. Many developers have seen the usefulness of chained exceptions, and made
their own
non-standard approaches. With JDK 1.4 this is no longer necessary.
12
4 Current Trends
As systems have grown more complex and customers’ demand for robustness has
increased,
more people have turned their attention to exception handling techniques. While
exception
handling in crude forms has existed since the mid 1960’s, the advanced exception
handling
techniques of today is a new field of research. There is no generally accepted
agreement on
how exceptions are to be used. However, more is written about exceptions every day,
along
with debates about what exceptions really are for [EJE2003] and if checked
exceptions are a
good idea or not [DJN2003]. In this chapter we have gathered the recent information
to make
an overview of the current trends in exception handling today.
4.1 Designing for robust Java programs with exceptions
According to [EHO2003], the development of modern object oriented systems tends
toward
higher complexity and an increasing number of exceptional situations. Exception
handling
techniques are employed to deal with these problems, but there are some serious
issues when
applying them in practice that have yet to be solved. Exception handling is often
not
addressed at the appropriate phases of system development. Exception code design
and
analysis is complex, and methodologies supporting the proper use of exception
handling are
few and far between. We will take a look at these challenges in this section.
Addressing exception handling early
Exception handling is an important part of a large system. Handling exceptions in a
good way
helps debugging during development and increases the robustness of the finished
product. For
most projects these advantages are too important to haphazardly leave the exception
handling
up to each individual programmer. It is the system architect’s job to define how
the system
should respond to undesired events. [EAE2003] writes: Inexperienced programmers
tend to
invent and implement ad-hoc repair measures. If this happens at a large scale, the
system is
doomed to failure.
Still, for many projects the exception handling is pushed too late into the
development
process. Decisions that normally belong to the architectural or detailed design
phase are done
during implementation. Java’s creator James Gosling says this is a culture thing,
and blames
some of these problems on the way Java and exceptions are taught [FAE2003].
Exception
handling is the last mechanism to learn, and the last mechanism to use.
A lack of notations and patterns
According to [EHO2003] there is a lack of methodologies supporting the proper use
of
exception handling. This helps to explain why exception handling is not always
considered
during the design phases of development. With no tools available, it is easy to
forget the task.
With no standard notation, you need to create and agree upon a notation to include
exception
handling in your diagrams. With no support for exception handling in the standard
notations
used, you are breaking the standard to include them.
13
The Unified Modelling Language (UML) is the industry-wide standard for modelling
software architectures, and up until recently you could not describe the logic flow
of
exceptions with UML. This has been mended in UML 2.0 [UML2003]. An exception
handling notation has been introduced, and hopefully this will help developers
account for
exceptions when designing their software. A short introduction to this notation
will be
presented later in this chapter.
Along the same lines, there are few available patterns for the use of exceptions.
Patterns are
well-known techniques, abstractions of common design occurrences, which document
specific
reoccurring problems and solutions. For thorough information about patterns, see
[GAM1995]. Searching the large collection of patterns for Java [PIJ2001] we found
not a
single pattern for using exceptions, although there were a few for increasing
program
robustness. A pattern called safety facades was introduced by [EAE2003]. In the
next section
we will give a brief explanation of it.
Safety facades
A special case of the facade pattern [GAM1995], called safety facades, is presented
in
[EAE2003]. At the time of writing, this is to the best of our knowledge the only
pattern that
deals with exceptions at an architectural level. We give a short introduction to
the pattern
here.
A call between two components can be safe or unsafe. An unsafe call is done
directly, with no
exception handling in between. These two components form a risk community. If one
fails, so
does the other. A risk community consists of all components that are linked by
unsafe calls.
Safe access to risk communities is provided by safety facades. The safety facade is
responsible for exception handling. Exceptions within the risk community fly over
all
involved components and are finally caught by the safety facade. This means that
all
components within a risk community share the same exception handling mechanism. It
is
important to design the risk communities carefully.
A system will have several layers of safety facades. Exceptions are handled by the
nearest
safety facade. The exceptions are resolved there or propagated to the next safety
facade. The
outermost safety facade will shut down the program if unable to deal with the
exception.
Defining safety facades and risk communities at the architectural design phase has
several
advantages. The system architect gains control over the big picture of exception
handling in
the system. Programmers know which components are responsible for taking care of
which
exceptions.
Modelling exceptions in UML 2.0
Presented in UML 2.0 is a mechanism for describing how exceptions are handled,
especially
for describing the logic flow. With this notation we can use UML 2.0 to model
exceptions
with the use of class and interaction diagrams. The new version of UML also
includes new
developments in the activity diagram that deal with exception handling. An
exception handler
14
in the activity diagram specifies something to execute when a given exception
occurs during
processing.
Figure 4.1: Model of exception handling notation in UML 2.0
Figure 4.1 shows what a model of exception handling notation looks like in UML 2.0.
Protected Node in the figure represents the try block in an activity diagram. The
catch block
represents the handler body. If an exception occurs, the set of handlers is
examined for a
possible match. If a match is found, the handler body is invoked and the handler
catches the
exception. If the exception is not caught, the exception is propagated to the
enclosing
protected node if one exists. For an example of using activity diagram for modeling
exception
handling in UML 2.0 see figure 4.2. Read [UML2003] for additional explanations.
Figure 4.2: The URL viewer example, from [UML2003]
With the new mechanism for exception handling in UML 2.0, the impact of using
activity
diagrams to communicate proper use of exception handling in the design of a system
is huge.
Most projects should consider the benefits of using activity diagrams in system
modeling.
Even though modeling with activity diagrams promotes functional programming, this
is a
good way for describing the design of exception handling. Activity diagrams have
roots in
flow charting and are often associated with functional decomposition. Since it was
believed
that they promoted functional programming, activity diagrams were not favored in
the
software system community. It is important to balance any modeling activity (like
use case
modeling and activity diagrams) that promotes functional programming with
activities that
promote good object-oriented design.
15
Interface versus implementation
Ideally a method can be seen as an abstract contract, the interface, and the code
as one way to
implement that contract. However, many programmers forget about the interface and
just
think in terms of the code. It is then easy to forget that the throws clause is
part of the
interface. An implementation change may result in a method being called that throws
a new
checked exception. That does not mean that the exception should appear in throws
clauses up
the call stack. If the exception is just added to the throws clause without any
further ado, the
implementation is driving the interface. A method’s throws clause should be
considered at the
abstract contract level.
Consider a method loadUserPreferences(int userid). The purpose of the method is to
find a
set of user preferences based on a given user identification number. Depending on
how this
method was implemented, it could throw such exceptions as SQLException,
FileNotFoundException, MalformedURLException or SocketException. Throwing all these
exceptions isn’t sensible, and throwing only one of them binds the implementation
prematurely. This is where exception translation [EFJ2001] comes in handy. The low
level
exception, for instance SQLException, is caught by the method. The method chains
the old
exception to a new high level exception, for instance
UserPreferencesNotAvailableException,
and throws it. If the implementation changes at a later date, the method interface
stays the
same, and the caller of the loadUserPreferences method is presented with an
exception that is
appropriate to the abstraction level.
4.2 Best practices for using exceptions
We have done some research about tips and guidelines available today and will
present them
in this chapter. The following tips and guidelines are taken from various articles,
forums on
the internet and several books: [EFJ2001], [PRJ1999], [DRJ2000], [EJA2001] and
[EIJ2000].
The tips and guidelines will be divided into three parts where the first part will
show you
when you should use exceptions. Next we will advise you on the distinction in the
use of
checked and unchecked exceptions. Finally, the gathered tips and guidelines will
show you
how best to use exceptions.
When should I use exceptions?
Programmers do not always know when to use exceptions and when not to use them.
When
exceptions are misused, the programs will perform poorly, confuse users and will be
harder to
maintain.
First of all, you should only use exceptions, as the name implies, for exceptional
conditions. If
your method encounters an abnormal condition that it cannot handle, it should throw
an
exception. Avoid using exceptions to indicate conditions that can reasonably be
expected as
part of the typical functioning of the method. Never use exceptions for ordinary
control flow.
16
Example 4.1 – Flow control
Bad idea Good idea
try {
int i = 0;
while (true)
a[i++].f();
}
catch(ArrayIndexOutOfBoundsException
e) {
}
for (int i = +; i < a.length; i++){
a[i].f();
}
Consider Example 4.1 where exceptions are being abused in a horrible way. Instead
of using a
for-loop to go through all the elements available in the array and then exit, the
code to the left
uses a while-loop. The while-loop terminates by throwing, catching and ignoring an
ArrayIndexOutOfBoundsException when it attempts to access the first array element
outside
the bounds of array.
Once you have used exceptions you should never hide them. If you hide an exception
then it
would be nearly impossible to trace back to where the exception has occurred and
find the
original cause of the method’s failure. Furthermore you should never ignore an
exception and
hope it will go away, because it won’t. A thread will terminate if exception is not
caught and
there will be no record to show that exception has occurred. Take a look at Example
4.2
where the following output is: “In main, caught: Third Exception”. First and Second
Exception will be ignored since //1 is hidden by exception thrown at //2. Exception
at //2 is
again hidden by exception at //3. One solution to this is to save a list of all
exceptions
generated and throw an exception that holds a reference to this list.
Example 4.2 – Hidden exception
class Hidden{
public static void main (String args[]){
Hidden h = new Hidden();
try {
h.method();
}catch (Exception e){
System.out.println(“In main, caught exception: “ +
e.getMessage());
}
}
public void method() throws Exception {
try {
throw new Exception(“First Exception”); //1
}catch Exception e){
throw new Exception(“Second Exception”); //2
}finally{
throw new Exception(“Third Exception”); //3
}
}
}
Even though using exceptions can help you make your code easier to read by
separating
functional code from error-handling code, inappropriate use can make your code
harder to
read. This means that you do not have to use exceptions for every error condition.
You should
consider when it is intuitive to use exceptions and when not to use exceptions.
17
Example 4.3 – Intuitive use of exception
Bad idea Good idea
int data;
MyInputStream in = new
MyInputStream(“filename.ext”);
while(true){
try{
data = in.getData();
}
catch(NoMoreDataException e){
break;
}
}
int data;
MyInputStream in = new
MyInputStream(“filename.ext”);
data = in.getData();
while(data!=0){
//do something with data
data = in.getData();
}
Take a look at Example 4.3 where it is more intuitive, easier and faster to return
zero rather
than using an exception. Besides, using exceptions are more expensive in terms of
the
resources needed. The more exceptions you create the more handling you need to do
and
exception handling mechanism demands a lot of resources.
Which exceptions class should I use?
There are a lot of standard exceptions provided by Java. In this section we will
concentrate on
the distinction in the use of checked and unchecked exception, also known as
runtime
exception. The main rule for using checked exception is to use them for conditions
from
which the caller can reasonably be expected to recover. If you are throwing an
exception for
an abnormal condition that you feel client programmers should consciously decide
how to
handle, throw a checked exception. When using a checked exception is inappropriate,
it is
better to use unchecked exception. One technique for turning checked exception into
unchecked exception is to break the method that throws the exception into two
methods, the
first of which returns a boolean indicating whether the exception should be thrown.
Even
though it is not always appropriate to do such transformation, it will make the API
more
pleasant to use where it is appropriate.
Example 4.4
//Transform the calling sequence from :
try{
obj.actio(args);
} catch(TheCheckedException e) {
//Handle exceptional condition
……………
}
// to this :
if (obj.actionPermitted(args)) {
obj.action(args);
}else {
//Handle exceptional condition
……………
}
You should use runtime exception to indicate programming errors. When using
unchecked
exception the compiler does not force the client programmers to either catch the
exception or
declare in a throws clause. All unchecked throwables you implemented should be
subclass
from RuntimeException either directly or indirectly.
18
How do I best use exception?
Favor the use of standard exceptions
As you may have noticed, Java provides a lot of standard exceptions and you should
favor the
use of these exceptions instead of creating new ones. There are many benefits for
using
preexisting exceptions. The main argument for using them is that it will make your
code
easier to read and use, since programmers will be dealing with exceptions that they
are
familiar with. Besides, using preexisting classes and fewer classes mean a smaller
set of
classes to deal with and less time spent loading them. If you find it inappropriate
to use
preexisting exceptions or the exceptions you make are more convenient for your
code, then
you are better off creating new ones.
Catching exceptions
When catching an exception you should try to gather as much information about that
exception as possible. The more specific exception you throw the easier it is to
handle. This
means that you should try to avoid catching superclasses as Exception and Throwable
if you
can help it. Catch a superclass only if you are certain that all the exceptions you
will catch by
doing so, have the same meaning to your code. Always catch and handle
RuntimeException
or Error separately from other Exception or Throwable subclasses. Do not add
exception
handling at the end of the development cycle. Never create objects from the
Exception class
because an exception handler can not distinguish between the exceptions those
objects
represent. Remember to create exception objects from appropriate Exception classes.
Throwing exceptions
You should consider the type of exceptions to be thrown by a method from the
perspective of
the method’s caller rather than the class’s own perspective, when writing throws
clauses. If
you throw a bad type, the caller will not be able to handle the exception sensibly
and might
have to pass it on to its caller or the user. After finding out which type of
exception to throw,
you have to consider the object’s state to be thrown. The object has to return to a
valid state
before throwing an exception. The purpose of catching an exception is to try to
recover from
the problem that occurred and keep the system running. If you leave the object in a
bad state
the code will very likely fail anyway, even when the exception has been handled.
Consider
Example 4.5 where at //1 the method increases a counter for the number of objects
in the list.
If an exception is thrown after //1 then the object will be in an invalid state
because the
counter, numElements, is incorrect. The way to solve this problem is to move the
counter at
the end of the method after //2.
Example 4.5
class Foo{
private int numElements;
private MyList myList;
public void add(Object o) throws SomeException{
//…
numElements++; //1
if(myList.maxElements() < numElements) {
//Reallocate myList
//Copy elements as necessary
//Could throw exceptions
}
myList.addToList(o); //Could throw exception //2
}
}
19
If you have to deal with failure that occurs inside a constructor, throw an
exception object
from that constructor. Do not attempt to throw objects from any class apart from
Throwable
or a Throwable subclass, otherwise the compiler will report an error. The compiler
will also
report an error when a method attempts to throw a checked exception object but does
not list
that object’s name in the method’s throws clause. You should throw an exception and
never
throw errors.
Try/Catch/Finally
The catch keyword must appear immediately after a try block’s closing brace
character.
Placing try/catch blocks inside of loops can slow down execution of code as you can
see in
Example 4.6. Method2, which puts the try block outside the loop, has proved to be
more
efficient during execution than method1.
Example 4.6
Bad idea Good idea
public void method1(int size){
int[] ia = new int[size];
for(int=0;i<size;i++){
try{
ia[i]=I;
}
}catch (Exception e){
//Exception ignored on purpose
}
}
public void method2(int size){
int[] ia = new int[size];
try{
for(int=0;i<size;i++){
ia[i]=I;
}
}catch (Exception e){
//Exception ignored on purpose
}
}
Do not issue a return, break or continue statement inside a try block. If you
cannot avoid this,
be sure the existence of a finally does not change the return value of your method.
If a finally
block exists, it is always executed. The main benefit of using finally is to avoid
resource leaks
as you can see from Example 4.7. If an exception occurred in a try block, the close
call will
never be reached unless you have finally.
Example 4.7
Bad idea Good idea
class WithoutFinally{
public void foo() throws IOException
//Create a socket on any free port
ServerSocket ss = new ServerSocket(0);
try{
Socket socket = ss.accept();
//Other code here…
}catch(IOException e){
ss.close();
throw e;
}
close();
}}
class WithFinally{
public void bar() throws IOException
//Create a socket on any free port
ServerSocket ss = new ServerSocket(0);
try{
Socket socket = ss.accept();
//Other code here…
}finally{
close();
}
}
Create useful error messages
20
It is clearly an easy task to write throw or catch clauses, but writing good error-
recovery
information is more difficult. To write a good throw or catch clause is another
problem. We
will see here the benefit of writing good error-recovery information. Take a quick
look at
Example 4.8 where you can see a hard-code text string for throwing an exception. As
you can
see, this is a very commonly and undesirable way to build an error message. Most
developers
commit this type of mistake of placing explanatory messages with source code.
Example 4.8
if (!file.exists()){
throw new ResourceException(“Cannot find file” + file.getname());
}
The source code is not a place for explanatory messages, which are really more
related to
documentation. As long as the error messages are hard-coded among source codes,
they will
be created and owned by developers, often to the detriment of usability. Developers
should
not be writing error messages even more than they should not be writing
documentation. How
often have you seen error messages that are utterly incomprehensible except to the
person
who wrote the code?
21
5 Analysis Tools
In section 5.1 we take a look at JeX, a static toolkit for analyzing exception flow
in Java.
Section 5.2 describes Jikes Bytecode Toolkit, a class library written entirely in
Java that
enables Java programs to create, read, and write binary Java class files. Finally
in section 5.3
we try out Teamstudio Analyzer for Java, a best-practices audit tool that uncovers
errors in
Java code.
5.1 JeX
JeX is a static toolkit for analyzing exception flow in Java. It was developed by
Martin P.
Robillard and Gail C. Murphy in 1998 at the Department of Computer Science at the
University of British Columbia.
About JeX
JeX consists of four components: the parser, the Abstract Syntax Tree (AST), the
type system
and the JeX loader. The AST is used to identify the structures of classes and
exceptions within
a method or constructor. It also evaluates the expressions and invocations that may
cause an
exception to be thrown. The type system provides the AST with a list of all types
that override
a particular method. The parser component uses this for analyzing the exceptions in
the code
which JeX analyze. JeX loader loads the various components and connects them. See
[AEF1999] for additional information.
In order to use JeX, the user must specify a list of packages, a path to search for
JeX files and
a Java source code file in a configuration text file. JeX uses the following
template (see Table
5.1).
JeX template:
<jexpath> The root directory where JeX is installed.
<package> Package name to include for class-hierarchy analysis. Class hierarchy
analysis is
used to conservatively determine which method bodies can be executed for each
method call. As many package descriptors as desired can be used.
<genstub> Package name to generate stubs for at the beginning of an analysis.
<dir> Directory where the Java source file/files are to be found.
<file> The Java source file which JeX is supposed to analyze.
Table 5.1: Template for JeX configuration file
Additional information about JeX may be found at [JEX1].
JeX at work
We’ve tested JeX with some small programs, one large program and an example program
that
is available at JeX’s homepage. Before we were able to test JeX with any of the
programs, we
22
had to configure JeX and generate Java packages for JeX class-hierarchy. First of
all we
tested the example program that already had the configured text file that JeX uses.
JeX
worked fine in this example and there were no difficulties. The JeX-report that was
generated
by JeX only showed that there was one warning, which was expected, see figure 5.1.
Figure 5.1: The content of JeX-report.
Then we tested JeX on a program which was developed in 2000 by a group of students
at the
Norwegian University of Science and Technology. The reason for choosing this
program was
that it included a lot of exceptions. It was also programmed in Java and one of the
authors of
this report developed program. Here we encountered several problems. One of the
problems
was that JeX was developed with Java version 1.3 while the test-program was
developed in
Java version 1.4. Many packages included in Java 1.4 were not included in Java 1.3.
Another
problem was to set up the configuration file for JeX. The documentation found at
JeX
homepage was not clear enough to allow us to understand whether the set up was
right or not.
In the end, we gave up trying JeX with that program since we could not get JeX to
work with
it.
Then we chose a smaller program to try JeX on. This time all the packages needed
for
generating stubs in order to test out JeX, worked on this program. Surprisingly JeX
took an
extremely long time to go through a small program with only three Java classes. The
report
showed us a lot of warnings which shouldn’t appear. The warnings were about methods
it
didn’t find even though they were there, see figure 5.2. We were not able to
understand the
reasons why. Beyond this it seemed that the program was fine and JeX reported no
other
errors. We expected JeX to give advice on how to optimize our exceptions in the
code. It did
not.
23
Figure 5.2: JeX-report for our small program.
Conclusion
JeX is an old toolkit that will probably not work with most programs developed by
Java 1.4
and above. It may work if the packages used during development were included in
Java 1.3.
JeX has not been updated since 8 March 2002 which means it’s quite out of date. The
documentation should have been better and easier to read. The report which JeX
generates is
not easy to understand and did not show as much useful information as we had hoped.
JeX is
slow even when the program is small. For a huge program it will probably take JeX
hours to
finish. One thing that is positive about JeX is that the JeX-files which JeX
generates from
Java source file are very interesting. Here we can find all of the exceptions that
that method or
constructor in the Java source file may encounter. This will help the developer to
catch the
right exception when it is thrown.
JeX is best used during development since the user can insert the necessary package
much
easier than when the program is finished. We have found that it took us a long time
to go
through the source codes and insert packages for every file. JeX also makes it
easier to change
the code during development than changing finished products.
24
5.2 Jikes Bytecode Toolkit
Jikes Bytecode Toolkit is developed by Chris Laffra, Doug Lorch, Dave Streeter,
Frank Tip
and John Field at alphaWorks, IBM. It is a work in progress, but released versions
have
already been used in several projects.
About Jikes
Jikes Bytecode Toolkit is a class library, written entirely in Java, which enables
Java
programs to create, read, and write binary Java class files. It also lets the
programmer query
and alter a high-level representation of the collection of the class files, and
relations among
them.
Jikes provides a logical representation of the class file. Classes have methods,
methods have
code and code contains bytecode instructions. In Jikes these are represented by
objects that
can be read and manipulated.
From http://www.alphaworks.ibm.com/tech/jikesbt: Jikes Bytecode Toolkits’ model is
quite
rich and provides a number of relationships, such as an easy way of finding each
instruction
where a given class is allocated, where a given method is invoked, and where a
given field is
accessed. It records which methods override which others, which will be inherited
from other
classes, and even which method implements a method declared in a given interface
(even if
via inheritance).
Jikes and Exceptions
Using Jikes, a programmer can browse a class' hierarchy to find which methods throw
which
exceptions, and which methods catch them. This isn't unique to Jikes. What makes
Jikes
special is the added possibility of inserting code in the class files without
changing, or even
needing, the source code.
While the inserted code certainly can change the behavior of the program, this is
not our
intention here. Every throw, throws, try, catch and finally statement could have a
log writer
added. This would allow the program's execution and exceptional behavior to be
logged and
analyzed, without cluttering up the code or intruding on the program in any other
way.
Needless to say, this is work of much larger magnitude than this project. It might
be an
interesting work for a diploma. Since the analyzer would be independent of the
source code, it
could be applied to a many different programs. To best discover the behavior, the
modified
program would have to be inserted into a real situation - since that is where the
exceptions are
going to show up.
Conclusion
Jikes is an extension to the functionality of the Java programming language, and
not an
executable program. As such, it is not what we were looking for. However, Jikes may
prove
useful for creating such a program. It removes the need to write a code parser.
Class files can
25
be searched for exception-handling. It even lets you add functionality without
altering the
workings of the program. An application's execution can be observed and exception
handling
details written to a database.
5.3 Teamstudio Analyzer for Java
Teamstudio Analyzer for Java is a commercial product developed by Teamstudio, Inc.
This
section is based on a seven day free trial of the product and the contents of their
homepage
www.teamstudio.com.
About Teamstudio Analyzer
Teamstudio Analyzer is a best-practices audit tool that uncovers errors in Java
code. It is not a
standalone program, but is incorporated into different integrated development
environments
(IDEs). Among the supported IDEs are JBuilder, Eclipse, JDeveloper and Sun ONE.
Once the program is installed it will check your code against a set of best
practice rules. In
addition to about two hundred rules that come with the package, programmers can
write their
own rules and insert them into the rule set. These rules are written as Java
Classes.
When using the IDE after installing Teamstudio Analyzer, an additional window will
open.
This window contains a list of detected errors, a description and a link to the
line of code in
question. The rules range from brackets that aren’t closed to unnecessary imports
to missing
or incomplete Javadoc.
Teamstudio Analyzer and Exceptions
Some breaches of best-practices for using exceptions are simple to detect. These
are pretty
much covered by Teamstudio Analyzer. More complex scenarios are a lot harder to
find, and
are not part of the Analyzer default rule set. Figure 5.3 shows Teamstudio Analyzer
at work.
Errors discovered and reported by the Analyzer are:
? Empty catch and finally blocks.
? Throwable, Error or generic Exception caught.
? Class names ending with Exception that doesn't inherit from Exception, or the
other
way around.
? Method declares that it throws an exception that isn't thrown in the code.
? Unnecessary catch block, the code tries to catch an exception that isn't thrown.
26
Figure 5.3: Teamstudio Analyzer detects code violations.
Conclusion
While Teamstudio Analyzer doesn't have much support explicitly for exceptions, it
supports
writing your own rules (as Java classes) and integrating them into the analysis and
reporting
engine. However, since Analyzer is a commercial product, this probably isn't of
particular
interest to anyone that doesn't already use Teamstudio's suite of tools.
5.4 J2EE Code Validation for WebSphere Studio
IBM WebSphere Studio is a suite of tools for development needs, including Web
development, enterprise-scale application development and development for wireless
devices
[IWS2003]. This software includes several validation tools, but the most important
one of all
and most relevant to this report is the J2EE Code Validation Preview.
About J2EE Code Validation Preview
J2EE Code Validation Preview for WebSphere Studio (J2EE Code Validation) is a tool
that
automatically detects common error patterns and coding violations of best practices
in Web
applications [JCV2003]. IBM has categorized error patterns and coding violations
into
several, broad rule categories. J2EE Code Validation uses specialized analysis code
for each
rule category, and in this technology preview, detects over four hundred
violations.
These violations are very hard to detect by conventional means and they cause
serious
performance degradation and system outages in real-world production environments.
J2EE
Code Validation integrates with WebSphere Studio Application Developer and helps
developers identify and correct code defects early in the development cycle and
before
applications reach production.
27
J2EE Code Validation does not replace traditional code analysis tools. It augments
them by
conducting deep, static analysis of applications and by constructing all possible
paths and all
possible object manipulations. This type of analysis is more thorough and is
capable of
looking at a different set of problems. For example, using data flow analysis, J2EE
Code
Validation can find many common cases of rare conditions which source scanning
tools
cannot.
J2EE Code Validation and Exception
J2EE Code Validation does static program analysis (control and data flow) to check
for many
bad coding patterns. The analyzer points out where in the program the exception
handling has
been implemented poorly. Bad coding patterns include multiple exceptions handled by
a
single catch block, catch block not catching the exact exception thrown (but its
supertype) or
exceptions that have been swallowed etc.
Once the analysis is completed and if there are any coding violations, a list of
the result with
explanations will pop up in the task bar. You can also see the analyzed steps that
have been
taken during the analysis in the status bar. The user can click on the specific
item in the task
bar for a detailed explanation of the coding violation. In the explanation box
there will be
some highlighted text which you can click on. When you click on the text, the
source view
will show you which part of the code contains the coding violation.
You can also choose to see the paths that lead to the coding violation in the task
bar. A tree
structure will show you the paths in the application. When you click on it you can
see the
source code associated with the paths, figure 5.4 shows how it looks like.
Figure 5.4: J2EE Code Validation at work
28
Conclusion
J2EE Code Validation is a just a plug-in for WebSphere Studio, which means that you
have to
acquire the commercial product from IBM. There is a possibility for downloading the
trial
software which is over one GB. Even though J2EE Code Validation is a neat tool for
analyzing coding violation including wrong exception handling, it is more suitable
for people
who are already using WebSphere Studio. It takes some time to really get to know
the
software with its tons of functions. WebSphere Studio is Java software and it is
very slow
when starting up and requires at least 512MB of ram for the code violation to run.
5.5 Conclusion
Although these tools are very helpful, none of them fulfill the criteria for
handling exceptions
in a better way. First, JeX is out of date and only creates lists of inconsistent
methods. It does
not suggest how you could fix them or optimize them. Both J2EE Code Validation and
Teamstudio Analyzer detect bad or wrong use of exceptions, but they also do not
advise you
how to fix or make them better. Jikes is not a tool or a program, but is a
collection of class
libraries, which can help develop a Java exception analyzing program like JeX.
Table 5.2
gives a summary of disadvantages and advantages of the tools we have investigated.
Name Advantages Disadvantages
JeX - It’s free
- Detect error in method
- Include almost all
exception class
- Setup up own
configuration file
- Overlook several
problem with
exceptions
- slow
- bad documentation
Jikes - Free
- Includes all classes of
exception
- Able to insert code in
class files
- Not a toolkit or
program
-
Teamstudio Analyzer - Works with several
programs
- Detects several severe
exception violations
- Add new rules
- Require license
- Just a tool to integrate
with other
development program
J2EE Code Validation - Points out error at
source code
- Show paths lead to code
violation
- Check precondition of
methods
- Require license
- Require WebSphere
Studio
- User must be familiar
with WebSphere
Studio
Table 5.2 Advantages and disadvantages with the programs we have tested.
29
6 Rules
In this chapter we introduce a set of rules that we have assembled throughout our
work with
this project. With the waterfall model as the starting point (see Figure 6.1), we
have made
rules for using exceptions in each phase of the model. We hope with these rules, we
could
help system developers to be able to make more robust and reliable systems. Even
though the
main bulk of the rules belong to the implementation phase, rules in preceding and
subsequent
phases are also important.
Figure 6.1 Waterfall model, with the number of rules at each phase.
6.1 System Requirements Phase
SR1: Clarify time spent on system robustness.
Creating a robust system takes time and resources. It is important that the
developers and the
customer are clear from the start how important system robustness is to the
project, and thus
which resources should be allotted to this end. The developers can err on both
sides of this, in
one case creating a program the customer is not happy with – in the other, spending
too much
time and money on something the customer is not asking for.
System
Requirements
(1)
Architectural
Design
(2)
Detailed Design
(2)
Implementation
(16)
Testing
(1)
30
6.2 Architectural Design
AD1: Define risk communities and safety facades.
It is the job of the system architect to define how the system as a whole should
react to an
exceptional situation. In a project where exception handling is not considered
during the
architectural design phase, these important issues will be up to each individual
programmer.
Setting up risk communities and safety facades makes it clear for everyone which
modules are
responsible for which kinds of exceptions. See section 4.2 for details.
AD2: Create a system wide user alert system.
Centralize the handling of user alerts; otherwise you will end up with error
messages spread
all over the code. A good user interface demands that the error messages presented
to the user
are coherent and informative. To achieve this you need an easy way to inspect and
revise
them all, as opposed to searching through thousands of lines of code looking for
that one
strange error message your users have been getting. (See Code Excerpt 7.1.3)
6.3 Detailed Design
DD1: For each module, identify possible emergencies.
Look at which external sources the module depends on, and what would happen should
they
stop working. [EAE2003] defines an emergency as a situation where the programmer of
the
component doesn’t know what to do – there is no local help available: the component
where
the emergency happened is unable to solve the problem. Emergencies range from
programming errors to unreachable databases and crashed neighbor systems.
DD2: Define how each module should respond to an undesired event.
There are certain problems that the module will encounter, some the module may be
able to
solve and others that are impossible to handle. You have to separate these and
define which of
the problems that may be solved and which the module has to pass on to the safety
façade (see
rule AD1). It is important to limit the module to problems which it can handle.
6.4 Implementation
IM1: Do not use exceptions for control flow.
It is possible to use the exception handling mechanism for control flow also during
normal
program flow. This makes the code harder to read, harder to analyze, significantly
slower and
opens up for some horrible situations if an actual exception were to occur.
Exception should,
as the name states, only be used in exceptional situations.
IM2: Use exceptions in exceptional situations.
In languages with less powerful exception handling mechanism than Java, programmers
have
to signal an exceptional state through other means. Some common examples are
returning a
null pointer, the value ‘-1’ for normally unsigned integers or the empty string.
This is not
necessary in Java and should be avoided. If the caller of the method is unaware of
these (often
undocumented) peculiars, the program will most likely start behaving in unforeseen
manners.
Exceptions are part of the method interface and callers are forced to take them
into account.
(See Code Excerpt 7.2.2)
31
IM3: Do not catch the generic exceptions.
A method might throw several different exceptions, and some programmers would be
tempted
to simply catch the generic Exception class – especially if all the thrown
exceptions are
handled in the same way. Resist the temptation before you create a monster that
swallows all
exceptions thrown at it. All warnings that something has gone wrong will disappear
or be
masked as something else entirely. (See Code Excerpt 7.1.2 and 7.2.4)
IM4: Do not throw the generic exceptions.
By throwing the generic Exception class, you remove all power from the user of the
method.
The programmer has no way of knowing what has gone wrong, why it went wrong or what
can be done about it. In addition the user of the method is forced to violate rule
IM2, allowing
for even more problems. If there is no exception matching yours in the standard
exception
library, create your own exception. Never throw the Throwable, Exception or
RuntimeExcecption classes.
IM5: Ensure the object is in a stable state after throwing an exception.
Clean up with finally after a try clause so you’re always in a consistent state.
Protect yourself
on the way up the call stack by deallocating any used resources. When the object is
unstable
after the exception has been handled, it will most likely fail and throw exceptions
again.
Another case is when other objects depend on that object and use it in an unstable
state. This
might cause a myriad of exceptions. (See Code Excerpt 7.2.1)
IM6: Create abstract superclasses for related sets of exceptions
Regularly review existing exceptions usage and introduce abstract superclasses
where the
same corrective action might be used for a set of different exception classes.
Callers can then
name a single superclass in a catch block instead of all the individual exception
classes for
which a corrective action is appropriate. Making the superclasses abstract enforces
the
throwing of specific concrete exceptions
IM7: Do not leave a catch clause empty.
Only catch an exception if you can and will handle it. If you are unable to handle
the
exception then pass it on, but never ignore it. Leaving the catch clause empty will
trick the
user and the program into thinking that the exception has been handled while it did
nothing.
Ignoring exceptions means that you will lose vital information and you might not
even
register that an error has occurred. (See Code Excerpt 7.2.4)
IM8: Use runtime exceptions to indicate programming errors.
When an abnormal condition occurs, in situations where the client is unable to
handle
exception, you should use runtime exceptions. This is an error and does not force
the client to
catch nor to declare it in a throws clause. There is no point in handling runtime
exceptions
since you will not be able to recover from them.
IM9: Use checked exceptions for recoverable conditions.
If the user can reasonably be expected to recover from the exceptions that occur,
then use
checked exceptions. This forces the programmer to deal with the exceptional
condition.
Checked exception is also used whenever a method is unable to fulfill its contract.
The
contract includes preconditions that the client must fulfill and post conditions
that the method
itself must fulfill.
32
IM10: Be careful when returning from a try clause with finally.
Always remember that the finally clause will execute regardless of what happens in
a try
clause. So be careful when returning from try, since finally will produce a return
value which
will overwrite the return value from try. To avoid this pitfall, do not issue a
return, break or
continue statement inside the try clause, or just make sure that finally does not
overwrite the
return value of the method.
IM11: Use separate try blocks for statements that throw the same exceptions.
When several statements throw the same exceptions, there are no ways of telling
which of the
statements threw the exception. If you put each statement into separately try
blocks, the
debugging will be much easier. When an exception occurs you will know exactly where
and
which statement produced the exception. (See Code Excerpt 7.1.4)
IM12: Use standard exceptions provided by Java instead of creating new ones.
Using standard exceptions will make it easier for others to read since they will be
dealing with
exceptions that they are familiar with. Besides standard exceptions are already
well
documented which means that you do not need to document them again. If there are no
suitable standard exceptions to use then you are of course encouraged to make your
own.
IM13: Do not propagate implementation specific exceptions.
Throw exceptions that make sense in the context of the method. Consider a method
that can
throw a set of exceptions. Throwing all isn’t sensible, but throwing one of them
binds the
implementation prematurely. Higher layers should catch lower-level exceptions and
throw
exceptions that are more explainable in terms of higher-level abstraction. This is
called
exception translation [EFJ2001]. (See Code Excerpt 7.1.1)
IM14: Use exception chaining when translating an exception.
Once you have decided to translate an exception, you should always chain the old
thrown
exception to the new exception. In this way all exceptions are chained back to the
lowest-level
where the first exception was thrown. This makes it easier to trace back to the
source of the
problem. Debugging is also easier, since the entire stack trace is available.
(See Code Excerpt 7.1.1)
IM15: Use exceptions when a constructor fails.
When the initialization of an object fails, your only way to signal this is through
an exception.
Swallowing exceptions in a constructor will result in live objects that have not
been initialized
properly. Needless to say the behaviors of these objects are near impossible to
know in
advance. (See Code Excerpt 7.2.3)
IM16: Document the exceptions well.
Use Javadoc @throws tag to document each exception. Also document precisely the
condition
for the exception to occur. A good documentation of exceptions will make it easier
to handle
the exceptions that occur. It is usually only the developer who has written the
exceptions that
understands them, but with good documentation others will too.
33
6.5 Testing
TE1: Use a global flag to toggle debug information.
Many catch blocks will contain debug information, often in the form of
ex.printStackTrace().
This should not be part of the final build. Use a simple global flag to separate
debugging code
from normal code. It helps developers to clearly separate debug and normal code.
After
testing, turn the debug information off. You need not go through the code and risk
removing
too much or too little. Should it be necessary you can easily turn the debugging
back on.
(See Code Excerpt 7.1.1)
34
7 The rules in practice
In this chapter we review the code of two large real life projects, IpManager and
DIAS 2. We
look at how they have used exceptions in light of the rules in Chapter 6, pointing
out why it is
good, why it is bad, and what can be done to improve the code.
When starting this project, we intended do a real test of the rules we made. We had
plans to
create a test, apply our rules to a large project, and then test both the original
program and the
modified one to see if there was any improvement. In this way we would check if the
rules
improved the robustness of programs, or at least that particular program. We began
working
on our rule set and learned more about designing robust programs with Java
exceptions. After
many weeks we came to realize that our testing plan was not plausible.
Our single most important rule is that in order to create a robust program,
exception handling
has to be accounted for throughout the process. It can not be added as an after-
thought. Most
of our rules aren’t simple fixes – they are meant to force developers and
programmers to think
differently about exceptions. The application of our rules can change how the
entire system
works in an undesired state.
We opted for the second best alternative, examining and commenting on the program
code to
find examples where the rules are in use or ought to be used. We hope this will
help clarify
some of the rules, and give a better understanding of what can go wrong when using
Java’s
exception handling mechanisms.
7.1 IpManager
IpManager is an Open Source project founded by Mario Aparicio and Salten
Kraftsamband
AS in 2003. It is an application for managing networks, subnets and nodes within a
company
network. It helps the administrator keep track of names and descriptions of nodes
and
networks, and shows the number of nodes in a subnet with a certain network mask.
The Code Excerpts
Code Excerpt 7.1.1
public void saveDocument( Document doc, String filename )
throws SaveException {
try {
// method body
} catch( javax.xml.transform.TransformerException e ) {
if ( DEBUG ) {
System.err.println( e.getMessage() );
e.printStackTrace();
}
throw new SaveException( e.getMessage() );
}
}
35
Code Excerpt 7.1.1 shows in a good way how the rule IM13: Do not propagate
implementation specific exceptions works. The implementation bound
javax.xml.transform.TransformerException is translated to a SaveException that is
meaningful
in the context of the saveDocument method. This allows for the saveDocument method
to
change data representation later, without changing its interface. The
implementation is
separated from the interface, and the user of the saveDocument method is presented
with an
exception that makes sense.
The example also shows how the rule TE1: Use a global flag to toggle debug
information
can be used. The stack trace would only be interesting to the programmer. Using
this flag the
programmer can easily switch off debug information once the program is released. In
addition
it makes the code easier to read for others, since it is clear what is debugging
code and what is
normal code.
To further improve this particular piece of code we have two suggestions. One is
described in
the rule IM14: Use exception chaining when translating exceptions; the old
TransformerException should be chained to the new SaveException. This way the
entire stack
trace can be found for exceptions further out the call stack, and programmers
interested in
what caused the SaveException have a way to find out. Second, following the naming
convention for exceptions suggests that SaveException should be renamed to
SaveFailedException.
Code Excerpt 7.1.2
try {
// method body
} catch ( Exception e ) {
System.err.println( e.getMessage() );
e.printStackTrace();
throw new XMLParseException(
"Error when reading file:" + e.getMessage() );
}
Code Excerpt 7.1.2 shows a violation of the rule IM3: Do not catch the generic
exceptions.
This code excerpt will swallow and translate all kinds of exceptions to an
XMLParseException. Should an ArrayIndexOutOfBounds, NullPointerException or similar
be
thrown in the method body it would not be treated in any other way than the
exceptions the
programmer had intended to catch. Luckily the error message is echoed to the error
stream;
otherwise the bug fixer would have a colossal headache ahead of him.
To fix this problem, the programmer should map out what exceptions are thrown in
the
method body and catch those exceptions specifically. The program would function
like the
programmer intended, and unexpected exceptions would not be swallowed.
Code Excerpt 7.1.3
} else if( foundOne == true ) {
throw new IllegalNetmaskException(
"The netmask is illegal. Legal values should be: \n" +
"255.255.255.X\n 255.255.X.0\n 255.X.0.0 or\n X.0.0.0" +
"Where X is:\n 0, 128, 192, 224, 240, 248, 252, 254 or 255" );
}
36
Code Excerpt 7.1.3 shows the violation of rule AD2: Create a system wide user alert
system. The error message is hard-coded into the exception. This makes it
inconvenient
should you want to inspect, alter or reuse some or more of the error messages
presented to the
end user.
Gathering all error messages in one place has several advantages. Adding support
for different
languages based on user location, changing the user dialog for all messages or
making sure all
the messages are coherent and formatted in the same way – these changes and several
others
are made a lot simpler by having a centralized user alert system.
Code Excerpt 7.1.4
private TreeNode buildTreeFromXMLDocument( Document doc )
throws XMLParseException {
//some code
try {
for( int i = 0; i < networks.getLength(); i++ ) {
currentNetwork = (Element)networks.item( i );
networkN = new NetworkNode();
networkN.networkName = currentNetwork.getAttribute ( "Name" );
networkN.networkAddress = (java.net.Inet4Address)
java.net.InetAddress.getByAddress(
UtilityClass.extractIpAddress(
currentNetwork.getAttribute( "Address" ) ) );
networkN.netmask = UtilityClass.extractIpAddress(
currentNetwork.getAttribute( "Netmask1" ) );
networkN.onesNetmask =
Integer.parseInt(currentNetwork.getAttribute(
"Netmask2" ) );
networkN.description = currentNetwork.getAttribute(
"Description" );
networkTreeNode = new DefaultMutableTreeNode( networkN );
rootTreeNode.add( networkTreeNode );
addresses = currentNetwork.getChild Nodes();
// a lot more code like this
}
} catch ( Exception e ) {
System.err.println( e.getMessage() );
e.printStackTrace();
throw new XMLParseException(
"Error when reading file:"+e.getMessage() );
}
return rootTreeNode;
}
Code Excerpt 7.1.4 violates the rule IM3: Do not catch the generic exceptions. That
was
also the case for Code Excerpt 7.1.2, so the same comments apply here. However,
this one
also shows an example of IM11: Use separate try blocks for statements that throw
the
same exceptions. Note that the code within the try block in the excerpt is
shortened
considerably from the original source code. If an exception is raised anywhere in
this massive
block of code, the programmer trying to debug the code will not know where in the
try block
the error occurred. A common practice to work around this problem is to insert
debugging
lines between every line of code in the try block, to see where the execution of
the code stops.
37
A better and less crude way to get this information is to use several smaller try
blocks around
the critical code.
7.2 DIAS 2
Distributed Intelligent Agent System 2 (DIAS 2) is part of a diploma thesis by Bård
Smidsrød
Moen and Anders Aas at NTNU during autumn 1999. The diploma involves design and
implementation of a mobile agent architecture supporting interoperability. Central
issues in
DIAS are inter-agent communication, dispatching, disposing of agents and how other
agent
systems and clients interacts.
The Code Excerpts
Code Excerpt 7.2.1
try {
AccessController.beginPrivileged();
DEFAULT_USERNAME = System.getProperty( "user.name");
username = System.getProperty( "username", DEFAULT_USERNAME);
password = System.getProperty( "password", "");
} finally {
AccessController.endPrivileged();
}
Code Excerpt 7.2.1 shows the rule IM5: Ensure the object is in a stable state after
throwing an exception in practice. The finally block ensures that the resource with
privileged
access is unlocked regardless of the outcome of the method calls in the try block.
Note that
the try block is not followed by a catch block. This is not necessary, and in this
case the
programmer has no intention of mending an error at this point in the code. The
focus is on
making sure that the resource is unlocked.
Code Excerpt 7.2.2
public URL getAdpURL() {
try {
URL ret = new URL(adpName);
return ret;
} catch (MalformedURLException mue) {
System.out.println("Could'nt retrive the ADP's URL" +mue);
return null;
}
}
Code Excerpt 7.2.2 breaks the rule IM2: Use exceptions for exceptional situations.
The
code translates a MalformedURLException to a returned null pointer. The caller of
this
method has to check if the returned value is a null pointer, but this is not part
of the methods
interface, and has to be documented separately. If the caller fails to check for a
null pointer,
the program will crash with a NullPointerException once a malformed URL is used. If
the
38
caller checks for a null pointer, the decision to throw a new exception or fix the
situation has
to be made.
The same functionality is attained by simply propagating the MalformedURLException.
Exceptions are part of the method interface, so the possibly undocumented feature
of a
returned null pointer is avoided. If the caller of getAdpURL can not mend the
situation, that
method throws it further. The other solution would result in the first exception
being thrown,
caught immediately afterwards, translated to a returned null pointer, just to see a
new
exception thrown one step up in the call stack. This is not only awkward and hard-
to-read
code, it is very slow code.
Code Excerpt 7.2.3
public AMPInfo(String strAmpInfo) {
XmlDocument xmlInfo = new XmlDocument();
try {
// large constructor body
} catch (SAXException se) {
System.out.println("AgentInfo::AgentInfo:" +
"The agentID could'nt be parsed \n"+se);
} catch (IOException ioe) {
System.out.println("AgentInfo::AgentInfo:" +
"An IO exception has been thrown \n"+ioe);
}
}
Code Excerpt 7.2.3 shows an example of breaking the rule IM15: Use exceptions when
a
constructor fails. If a SAXEception or an IOException is thrown in the constructor
body, the
AMPInfo object will not be properly initialized. A warning is echoed to System.out,
informing
the user of the problem if the user is monitoring that channel. The program has no
way of
knowing something went wrong during the initialization of the object, and will
continue as if
nothing was wrong. Putting it a bit flippantly, a veritable exception generator has
been let
loose in the system. One or more NullPointerExceptions is likely to occur. Worse
still is if the
incomplete object does not trigger an exception, but is used by other subsystems in
good faith,
feeding them incoherent and false data.
Code Excerpt 7.2.4
public void parseMessage(String str) throws ParseException {
try {
KQMLStreamTokenizer parser = new KQMLStreamTokenizer(new
BufferedReader(new StringReader(str)));
parseMessage(parser);
} catch (Exception e) {
e.printStackTrace();
}
}
Code Excerpt 7.2.4 violates the rule IM7: Do not leave a catch clause empty. A
method that
only dumps the stack trace to System.out is empty for all practical considerations.
No matter
39
what happens further down the call stack, this method will print the stack trace
and end the
crisis; fixed or not. Imagine a program feeding tens of thousands of messages into
this method
while an underlying system is down. The System.out channel will be flooded with
messages,
and afterwards the program continues as if everything worked out as planned.
It also shows yet another example of the rule IM3: Do not catch the generic
exceptions (see
Code Excerpt 7.1.2 for a discussion), but this one has an interesting twist. The
method
declares that it throws a ParseException, but thanks to the catch clause that
accepts all kinds
of exceptions, this method will never throw that exception. In fact, it will never
throw any
kind of exception. This is obviously an oversight by the programmer, but it would
never have
been missed if the programmer was not tempted to catch the generic exception,
because then
the compiler would have complained.
There might seem to be an easy fix to this problem. Simply throw a new
ParseException in
the catch block. See Code Excerpt 7.2.5 for why this is not such a good idea.
Code Excerpt 7.2.5
public void parseMessage(KQMLStreamTokenizer parser)
throws ParseException {
try {
// method body
} catch (Exception e) {
e.printStackTrace();
throw new ParseException();
}
}
The method in Code Excerpt 7.2.5 is the one being called in line five in Code
Excerpt 7.2.4.
Notice that this catch block does in fact throw a ParseException. Any and all
exceptions that
are thrown in the method body will be translated to a ParseException, and all
information
contained in the old exception is lost. At the very least the rule IM14: Use
exception
chaining when translating exceptions should be followed, chaining the old exception
to the
new one.
Now take a look at Code Excerpt 7.2.4 again. What happens if the easy fix mentioned
there
(making it throw a new ParseException) is applied? The program will throw the first
exception, catch it along with all other general exceptions, and then throw a new,
nearly
identical exception. This is slow, it is cumbersome, it makes bug finding harder,
and it is not
necessary.
40
8 Conclusions and Further Work
For a long time, developers have ignored and misused the powerful error handling
mechanism
that Java offers. Being a third generation programming language, Java is one of the
few
programming languages that provide very good exception handling. Developers should
take
advantage of this and make better use of them and using them more often. Many
developers
tend to avoid using exception or use them as little as possible because they lack
the knowhow.
Other developers consider exceptions as afterthought or an as-you-go addition,
which
often results in difficult debugging and unstable programs.
Exception handling is an integral part of a robust system. It should not and cannot
be added as
an afterthought. The biggest mistake for most developers is that they deal with
exceptions far
too late in the development. Not until they have reached the implementation phase
do they
become aware of how important a role exception handling plays in the development
process.
To build a robust and reliable system, developers need to take exception handling
into account
throughout the entire development process.
It is important that developers understand and know how to use the exception
handling
mechanisms. This information is not always obvious and there is great potential for
improvement in this field. Not only do they need to know how to use this powerful
mechanism, they also have to understand how important a role it plays in the
development
process. Wrong use of exceptions can lead to severe problems, like bug ridden,
unstable
programs.
We present in this paper a set of rules to help developers reach their goals and
improve their
programs. We believe that developers following these rules will develop more robust
and
reliable systems. Developers are forced to take exceptions into account already at
the system
requirements phase. The sooner exceptions are taken into consideration, the easier
they are to
implement and to manage. The rules at each stage, from system requirements to
testing, are
equally important to follow.
We inspected two real life projects in Chapter 7, and saw that there was definite
room for
improvement in the exception handling of both projects. If the projects were to
abide fully by
the rules in this paper, large parts of the code would have to be completely
rewritten. This
made it not only prohibitive for us to test our rules formally, but it emphasizes
the importance
of designing with exceptions in mind from the start.
Further Work
We were not able to formally test our rules by modifying an existing program (see
Chapter 7).
This makes it very interesting to see how the rules fare in a case study. An idea
is to follow
two groups of developers during their process of developing a program. One group
gets a
presentation of this paper and the rules, and is encouraged to comply with the
rules. The other
group is not asked to do this, but is simply watched. Following the two groups
through all the
phases of development, and looking at the robustness of the final products, might
give some
interesting insights into what part exception handling plays in the stability of a
program and if
the rules actually help developers create better programs.

S-ar putea să vă placă și