Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 2 of 3
It's unwise to simply declare a method as throwing all the exceptions that might be thrown by methods it calls. Not only is
this an abdication of design responsibility, but it exposes a method's implementation as part of its interface, limiting your
ability to modify the implementation in the future. For example, suppose the getResource method can load a resource from a database, from a file, or from a remote server -- exactly where depends on the resource
name and how the ResourceLoader object was initialized. If you just let exceptions thrown by called methods propagate out to your callers, getResource() could throw IOException, SQLException, and RemoteException, among others. But will all these different exception types prove useful to the code that calls getResource()? If a method calls getResource() because it needs to load a resource, that method is unlikely to be able to take corrective actions that distinguish between
an SQLException and an IOException. From the perspective of the calling method, both exceptions simply mean that the resource couldn't load. Therefore, it probably
makes more sense to just throw a single ResourceLoadException exception type.
There is no hard-and-fast rule about how many exception types a method should throw, but fewer is generally better. Each exception
type a method might throw will have to either be caught or thrown by any method that calls it, so the more different types
of exceptions a method throws, the more difficult that method will be to use. If you throw too many exception types, callers
might get lazy and simply catch Exception -- or, worse, throw Exception. These are dangerous practices; callers should instead treat exception types individually. Throwing more than three different
exception types generally indicates a problem: the method either performs too many different tasks and should be broken up,
or lazily propagates lower-level exceptions that should either be mapped to a single higher-level exception type or caught
and handled within the method.
When writing a throws clause, for each distinct exception class that you consider throwing, you should ask yourself: "What would a caller do with
this exception? Could the caller possibly take corrective action based on the exception type that differs from any other exception-handling
action?" If the answer is no, then that exception should either be caught and handled by your code or translated to another
exception type that more closely captures the error type.
There is another significant advantage to throwing a small number of higher-level exception types instead of many individual
low-level exception types: it prevents the throws clause from changing every time the method changes.
When method signatures change, they often trigger modifications in classes that call them. The impact of those modifications
can range from mildly annoying to disastrously inconvenient, depending on how many classes must change and on how many different
people or organizations use the altered class. The throws clause provides a vital part of the method signature, and you should take care to ensure its stability. Adding a new exception
class to a method's throws clause means that every class using that method must now also catch the new exception or modify its own signature to indicate
that the new exception might also be thrown. Clearly, this sort of instability is undesirable and costly. The best way to
avoid this problem is to prevent it -- by initially specifying that methods throw exceptions consistent with what they are
actually supposed to do, not just with how they are currently implemented. Instead of adding a new exception class to a method's
throws clause, try to group related exception types into an object hierarchy and include only the parent exception type in the throws clause. The IOException class from the java.io package provides a good example. More specific exception classes, such as EOFException, are defined as subclasses of IOException, but nearly all the methods in the java.io package are defined as throwing only IOException.
Subclassing exceptions from a common parent allows exception handling to proceed in a more object-oriented manner. Callers
can choose to treat all IO-related exceptions equally with a single catch block by simply catching IOException. But if callers can deal with a specific type of IO-related exception in a specific manner, they can catch the more specific
exception type first and recover appropriately.
Grouping exception types particular to a package or application together by subclassing them from a common parent improves
the stability of methods' throws clauses. You can add specific exception types to the package in future versions without affecting existing methods' signatures,
which in turn means that you don't need to change client code every time you add a new exception subclass.
When your code encounters a low-level exception whose type information would not be meaningful to your callers, translate the exception into one that makes more sense. For example:
public class ResourceLoader {
public getResource(String name) throws ResourceLoadException {
try {
// try to load the resource from the database
...
}
catch (SQLException e) {
throw new ResourceLoadException(e.toString()):
}
}
Now, both the program and the user receive the information they need. The caller learns that the resource could not load;
this likely provides a more useful type of error than the underlying SQLException, since ResourceLoadException more closely matches the type of operation the caller performed. But the explanatory message from the low-level exception
is preserved, so the program can still provide the user with a more specific explanation as to what went wrong.
You can extend the above technique by providing an alternate constructor for ResourceLoadException, one that accepts an Exception as an argument in addition to an error message. Wrapping one exception within another offers a powerful technique (sometimes
called exception chaining) for managing complexity while preserving state information. In Part 2 of this series, I'll present a specific technique for exception wrapping that provides the right information to all three
interested parties -- the calling method, the developer who must debug the problem, and the user who will encounter it.
Though the program doesn't care what text you place in the exception message (and programs should never look at the exception message text and use it to determine what happened), the user does. Error messages should be meaningful both to developers (so they can know more about the error's source and cause) and to users (so they have an inkling of why the program failed). Error messages like "Bad index value" or "Error in initialization" offer no help.
In order to ensure that users understand error messages, you also should think about who those users might be. Will they all speak the same language as the developer? If not, you should use an appropriate method for loading error message strings or templates from some localization mechanism, such as resource bundles, so that error messages can be easily translated into other languages without any text editing.