|
9/23 Recitation 3 - Some Rules and Some Exceptions
- Style Points
- Think Twice Before Using the @requires Tag
- Demonstrating Failure
- Do Not Leave catch Blocks Empty
In the old days, terminals were only 80 characters wide,
so programmers would limit lines of source code to 80
characters in length so that they would fit on the screen.
Although we do not use such restrictive terminals today,
the 80 character rule is still honored so that
programmers do not have to use the horizontal scroll bar
to read their code.
(This also makes it easier for your TA to read
your code when he prints it out
this is true for .txt files, too!)
In Eclipse, you can use Ctrl+Shift+F to Format
all of your source code so that it obeys the 80 character rule
(although it will not reformat your Javadoc comments if they
are too long). If you look under the Java preferences, then
you will discover that Eclipse provides you with more formatting
options than you could ever want.
You will probably be interested in setting the option
that tells Eclipse whether to put your braces like this:
public void iLikeMyBracesThisWay() {
}
or like this:
public void isntYourJavaFileLongEnoughAlready()
{
}
The other options, such as how many lines to skip between methods,
will probably be less interesting to you, but it's comforting to know
that they're there if you want them.
In last week's recitation notes,
I made a point about strong versus weak specifications.
To review, which clauses should be included in the Javadoc of a sqrt(x) method?
A
| @requires x >= 0 |
B |
@throws IllegalArgumentException when x < 0 |
C |
Both A and B |
The correct answer is B because it yields the strongest specification.
A is incorrect because it produces a weak specification for sqrt(x).
C is incorrect because A is not really true when B is true.
Granted, you are expected to only use sqrt(x) when x >= 0,
but you should not say that you can't if you can tell the user what will happen
if he does.
To find out what the expected input domain is, then look through the
@throws tags to see which inputs throw exceptions.
If there are many throws tags, and the set of expected inputs is convoluted,
then the expected input domain should be explained in the class overview;
however, it should NOT be expressed in the @requires clause,
as that would weaken your spec.
Reserve the use of the @requires clause to methods where conditions
are prohibitively expensive to check and therefore serve as a true requirement.
For example, consider the following:
/**
* @requires a is sorted
* @return int i where either i < 0 or a[i].equals(element)
* @throws IllegalArgumentException if a is null
* @throws ClassCastException if the array contains elements that are not mutually comparable
*/
public int binarySearch(Object[] a, Object element);
The whole point of a binary search is that it takes advantage of a sorted array
to produce a lookup algorithm that runs in O(log n) time.
By comparison, checking if an array is sorted takes O(n) time,
so doing this check would eliminate the advantage of using binary search!
Further, because the behavior of the method is truly unknown when the input violates this requirement,
it gets listed in the @requires clause.
For example, if you have:
int[] a = new int[] {95, 48, 7};
int result = Arrays.binarySearch(a, 48); // result is 1
then result will be the right thing.
On the other hand, if you have:
int[] a = new int[] {95, 48, 7};
int result = Arrays.binarySearch(a, 7); // result is -1
then you will get the wrong thing.
Thus, the need for the array to be sorted ought to appear in the @requires
clause: as the resultant behavior is unknown when the array is not sorted, it cannot be explained
in the @return, @effects, or @throws clause,
so it must appear in @requires.
Note that this is rarely the case, so think hard before deciding to add
an @requires clause to a public specification.
As stated in lecture, there are three ways to indicate errant behavior in a method:
- return a special value
- throw an exception
- invoke System.exit(-1)
Returning a Special Value
Returning a special value is often done because it is fast and convenient.
It is fast because it does not require the overhead associated with
creating a new Exception object and then throwing and catching it.
Further, it is convenient because it does not force the client to create
a try/catch block. This is acceptable practice when there is an
acceptable special value available to return (such as null or -1),
and that the error is not so drastic that you need to call the programmer's
attention to it (such as when List.get(Object o) fails to find o).
Throwing an Exception
Sometimes throwing an exception is necessary to indicate a failure because there
may be no special value available to return. For example, consider the following
method:
public int max(a, b); //returns the max value of a or b
Though it is hard to imagine how this method could fail,
it could not return a special int even if it did because
every int is a potentially valid return value for max().
However, the lack of a special value is rarely the main motivation for deciding
to throw an exception. Often, it is used to call the programmer's attention to
an unexpected failure and to give him some information to help him recover from it.
An Exception is an object just like any other, so it can have its own fields
that can be used to store information about the failure that the programmer can use
to help him figure out how to proceed.
For a deeper discussion of deciding when to throw exceptions, see
Effective Java as well as the lecture and recitation notes.
Invoking System.exit(-1)
This is just about the worst thing you can do and is probably completely unnecessary
you had better be damn sure that throwing an exception would not be acceptable instead.
But if you do decide to do something like this, then be aware that the integer
that you pass to the exit method should be some nonzero number since
exiting with zero corresponds to a clean termination. Often, the integer passed to exit
is supposed to correspond to some sort of error code, though as you know from 6.033,
numerical error codes are rarely helpful.
Here is a real example of where using System.exit() had disastrous results:
when I worked at Lockheed Martin in the summer of 2002 (right after I finished 6.170),
I was supposed to develop a user interface to wrap around this simulation that they had written.
They designed it so whenever their simulation threw an exception,
they decided to invoke System.exit(). Thus, you could
find the following all over their code:
try {
// thing that may throw an exception
} catch (Exception e) {
System.exit(-1);
}
When I asked them why, they argued that, "Well, since we can't recover from it,
we figured that it should just crash."
Brilliant. Just brilliant.
Thus, when my GUI called their simulation code, and the simulation threw an exception,
System.exit() was invoked, bringing down the simulation
as well as my GUI. Had they decided to let the exception be thrown,
then I could have caught the exception and displayed an alert box that said,
"There was an error during the simulation," or something to that effect.
(Had they thrown meaningful exceptions, then I could have given the user
a better message.) In any event, this would at least have saved the user
the trouble of restarting my GUI every time there was a problem with the simulation.
When you catch an exception, do something with it! Specifically, do NOT write the following:
try {
doDangerousThingThatMightFail();
} catch (Exception e) {
// meh.
}
At a minimum, you should at least call e.printStackTrace() in the
catch block so that you make yourself aware of the problem
(and can hopefully fix it later), or leave yourself
a TODO in Eclipse so that you remember to come back to it later
and put something useful there.
No catch block left behind!
|