This is the seventh post in the series of posts based on the book Effective Java by Joshua Bloch. In the previous post we read about General Programming practices to write good code in Java.
This section speaks about the best practices in handling Exception. This is one area that many do not understand properly and many of the problems arise due to lack of handling Exceptions.
This section speaks about the best practices in handling Exception. This is one area that many do not understand properly and many of the problems arise due to lack of handling Exceptions.
Use Exceptions Only for Exceptional Conditions
Do not use exceptions to handle situations which can be handled without
exceptions. E.g
try {
while(true) {
a[i++] .f();
}
catch(ArrayIndexOutOfBoundsException e) {
}
Instead use
for (i = 0; i < a.length; i++) {
a[i].f();
}
}
Exceptions as the name indicates should be used to handle Exceptional
situations; they should never be used for ordinary control flow.
Use checked exceptions for recoverable conditions and run-time exceptions
for programming errors
There are three types of exceptions in Java
1.
Checked Exceptions: These are exceptions that
can be handled by the client. E.g. IOException, FileNotFoundException. The compiler will force the user of the API
to handle such exceptions. I.e. the user needs to either declare it in the
methods throws clause or needs to put the code within a try – catch block where
the Exception is handled.
2.
Run Time Exceptions: E.g. ArrayIndexOutOfBoundsException,
NullPointerException. These are subclasses of java.lang.RuntimeException. The
compiler will not force the user of the API to handle such exceptions.
3.
Errors: These are serious errors from which the
users cannot be excepted to recover. OutOfMemoryError is an example of this.
These are subclasses of java.lang.Error.
The Checked Exceptions should be mentioned in the throws
clause so that the user of the API can handle it or pass it on to its caller to
be handled by it.
Runtime exceptions should normally not be thrown or handled.
But run-time can be used to indicate programming errors. E.g. one could throw
an IllegalArgumentException if any of the parameters passed to the method does
not meet the preconditions expected of it.
Errors should normally never be thrown or handled by any
method.
Avoid unnecessary use of checked exceptions
A method should throw
as less a number of checked exceptions as possible. Throwing more checked
exceptions makes it difficult for the user of the API to use the API and the
user ends up handling a generic Exception. Throw checked exceptions if and only
if the user of the API can recover from the exception that has been raised.
If some exceptions can be prevented by providing APIs for
checking conditions that can throw the exceptions provide this function so that
the user can use this API instead of handling a RunTimeException. E.g. if a
method of a class can be invoked conditionally then provide a method by which
the user can check if the method can be invoked before invoking the method so
that we can throw a Run Time exception from the method if there is an issue.
E.g. instead of forcing the user to write a code as follows:
try {
obj.action(args);
} catch(MethodAccessDeniedException e) {
//Handle Exception
}
}
We could provide a method called has Access and allow the
user to write code as
if (obj.hasAccess(args)) {
obj.action(args);
} else {
} else {
//Handle no access
situation
}
If a user invokes obj.action(args) without first checking
for has Access then the user may get an RunTimeException.
Throw Exceptions Appropriate to the Abstraction
Sometimes if APIs throw Exceptions which are seemingly out
of place it can be very confusing. This typically happens if one were to throw
an Exception generated at the lower level directly to the upper level. To avoid
this, higher layers should catch lower level exceptions and in their place
throw exceptions that are explainable In terms of the higher-level abstraction.
When doing this it is important to ensure that the lowest level of Exception
and the stack trace associated with it is not lost. This can be achieved by
using Exception chaining.
try {
….
} catch(LowerLevelException e) {
throw new
HigherLevelException(e);
}
Document all Exceptions thrown by each Method
Always document the exceptions that will be thrown by
method. Never use a generic exception like java.lang.Exception or
java.lang.Throwable to indicate the Exception thrown. This gives the user of
API no idea of what actually is the Exception that is being thrown from the
API.
1.
Document every checked Exception that the method
can throw using the @throws tag and include them in the throws keyword in the
method declaration.
2.
Document every unchecked Exception that the
method can throw using the @throws tag, but do not declare it in the throws
keyword along with the checked exceptions in the method declaration.
3.
If an exception is thrown by many methods in a
class for the same reason then it is not wrong to document this at the class
level indicating the reason for the methods throwing this exception.
Include failure-capture information in detail messages
When a program fails due to an unhandled exception the
system automatically prints the stack trace which includes the detailed message
in the Exception. This will typically be the only information somebody has to
figure out what went wrong. So to make this comprehensive the message should
contain the all the parameter values and field values that contribute to the
exception. One way to ensure that the Exception has all the details required is
to create a constructor which forces the user to provide the required
information by default.
Strive for failure atomicity
Generally speaking, a failed method should leave the object
in the state that it was in prior to the invocation. A method with this
property is said to be failure atomic.
The ways to achieve this are
1.
Make the class immutable. This will ensure that
the object is never altered irrespective of the exception thrown from any
method
2.
Check the parameters and fields for validity
before proceeding with the method execution. This will ensure the exception is
thrown even before the object is mutated.
3.
Another option is to write a recovery code which
can be invoked before an exception is throw to restore the object the state
prior to the invocation of the method that is throwing the exception.
4.
The last option is to make a temporary copy of
the object and work on it and if no exception occurs restore the new state to
the original object. This can be an expensive operation and should be done if
an only if really necessary.
This should be avoided if it makes the code more complex or
if it drastically reduces the performance of the system.
Don’t ignore Exception
Of an exception is caught then it should be handled correctly.
One should not have a code as follows:
try {
…
} catch (SomeException e) {
}
An empty catch block defeats the purpose of exceptions which
is to force the user to handle exceptional situations.
At the minimum have a comment explaining why the exception
is being ignored.
In the next post we will read about best practices of using Threads in Java.
In the next post we will read about best practices of using Threads in Java.
No comments:
Post a Comment