I've been enjoying, "Java: The Good Parts" by Jim Waldo and just finished chapter 3: Exceptions. At the end of the chapter, Mr. Waldo takes a humorously firm stance against RuntimeExceptions. Quick review: checked exceptions have to be declared in the method signature and dealt with by the calling code; RuntimeExceptions don't. Indeed, RuntimeExceptions can be evilly misused. Yet I believe there is a time and a place for these exceptions. Knowing when to use them requires an understanding of who is at fault for a particular problem.
The following code sample bit me the other day:
public static void write(String s) {
try {
out.write(s);
} catch (IOException ioe) {
throw new IllegalStateException();
}
}
It is evil (as Mr. Waldo says) for several reasons:
- This method is not being responsible for handling its own problems. IOExceptions happen for good reasons other than coding errors - users can delete files, network connections can close, etc. It could retry the write() or recover some other way. It could let the original exception bubble up to the caller, or it could report the error some other suitable way (e.g. to the user).
- Wrapping a checked exception with a RuntimeException means that this method signature is missing vital information that the caller really needs to know about. It hides critical details (any problem with the write() now causes an unexpected exception).
- Absentmindedly wrapping an exception with another exception serves only to complicate the stack trace - it further hides the cause of the problem. It's good to wrap an exception if you have critical information to add (e.g. the method handles two streams and you wrap an exception with the message of which stream it applies to). In the example above, nothing is added. In fact, this code doesn't wrap an exception, it throws it away and substitutes another which is even worse.
- The IllegalStateException is blank, giving the unfortunate caller no idea what went wrong.
The following shows proper usage of RuntimeExceptions:
/** Creates or returns an existing immutable YearMonth object.
@param year a valid year
@param month a one-based month between 1-12
@return the relevant YearMonth object
*/
public static YearMonth valueOf(int year, int month) {
if ( (month < 1) || (month > 12) ) {
throw new IllegalArgumentException("Month must be a positive integer such that 0 < n < 13");
}
...
This works because:
- Valid input values are documented clearly.
- The inputs are checked at the beginning of the method, before any processing is done.
- It throws a RuntimeException to inform the caller that they have made a coding error.
- The exception provides information about what the caller did wrong.
You wouldn't want to use a checked exception because it would force the responsible caller's code to check their input values twice:
YearMonth ym;
if (m > 12) {
out.write("Month too big");
} else if (m < 1) {
out.write("Month too small");
} else {
try {
ym = YearMonth.valueOf(y, m);
} catch (Exception e) {
out.println("month still invalid: " + e.getMessage());
}
}
The critical nuance here is that RuntimeExceptions should be used to indicate a programming error on the part of the caller of the function that throws it. They are generally used in the first few lines of the function to check for invalid input values. Each RuntimeException should include a description of the problem (not be blank).
RuntimeExceptions can also be used to catch invalid state as follows:
enum Numb {
ONE,
TWO;
}
private Numb num = null;
public void init(Numb n) {
if (n == null) {
throw new IllegalArgumentException("init cannot take a null Numb");
}
num = n;
}
public void showNum() {
if (ONE == num) {
out.println("1");
} else if (TWO == num) {
out.println("2");
} else {
throw new IllegalStateException("Unhandled value of Numb or called showNum without initializing the num");
}
}
When some programmer adds THREE to Numb and doesn't account for the new possible value, it fails hard and fast, making the problem easy to find and fix.
The Java compiler and a good IDE can catch many errors for us, but as a second or third line of defense, RuntimeExceptions are a good way to make code fail hard and fast without forcing the caller to check their input values twice. Used properly they can make sneaky coding errors obvious. Used improperly, they can make and obvious errors sneaky.
0 comments:
Post a Comment