So let's consider an example of a very simple program. What could go wrong?
Obviously, this is a very simple program. As far as what
could go wrong ... lots of stuff. Try some of these.
~/$ java Ex1 1,2,3 78 ~/$ java Ex1 -1,-2,-3 0 ~/$ java Ex1 1,0,3 2147483647 ~/$ java Ex1 1,foo Exception in thread "main" java.lang.NumberFormatException: For input string: "foo" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:492) at java.lang.Integer.parseInt(Integer.java:527) at Ex1.main(Ex1.java:15) ~/$ java Ex1 , 0 ~/$ java Ex1 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at Ex1.main(Ex1.java:12)The first call is OK. Everything else has an error ... some of which the program is telling you about, others not. Can you identify all the things that could go wrong here? |
In fact, to be a bit more representative of real programs, let's assume that the we have things broken up into functions and maybe even split into different classes. We might get a situation like this.
Of course all the same things could go wrong, but now they can go wrong in different places: some problems crop up in main, some in the getSF method, and some in the compute function that's in a totally separate class! So if we want to actually take care to account for these kind of things and act appropriately in every situation ... what can we do? Now we start to see some of the problems that handling errors (or "exceptions") present us with. Here are a few:
The moral of the story is that error handling is difficult. The article "Exception Handling in C++" by Bjarne Strouptrup (the author of the C++ language) is a really great read. It talks about a lot of the difficulties inherent in error handling, and it explains the design of C++'s exception handling mechanism, on which Java's is strongly based. He identifies four standard approaches to exception handling prior to the design of the C++ mechansim:
Throwable, which is a class in the Java API.
This special alternative way to exit a function is
acheived with a "throw"
statement. Like this:
throw new Throwable();We refer to this as "throwing an exception" (or "error" in some cases).
In the code that called the function or evaluated the expression that caused the exception to be thrown, we need some way to capture that throwable object. This is accomplished with a try-catch block.
try {
... regular old code
}catch(Throwable e) {
... code to exectue if an exception was thrown in executing the "regular old code"
}
The semantics (meaning) of this is that the regular old code
in the try block is executed as usual. If no exceptions are thrown
while executing this code, the "catch" block is simply ignored. If,
however, an exception is thrown at some point, the "regular old
code" following that point is not executed. Instead, control jumps
to the "catch" block, and the code in it is executed. This is
where you deal with whatever the problem was that caused an
exception to be thrown.
throw new Throwable();... for cases when the checks fail, with one little catch. Just as the compiler needs to know what the name, return type and parameters are a for a function, Java needs to know what exceptions a method might throw; and it expects that fact to be made specific. How? By adding a "throws clause" to the end of the prototype.
With this Ex2, for example, no longer compiles, but for an interesting reason: the method getSF() calls SpecialFunc.compute(), which might end up throwing an exception, without making any provision for the possiblity. That, as it turns out, is a no-no! With a caveat that we will address shortly, any Java method that includes code that might result in an exception being thrown must either catch the exception, or pass it along to the next function down the call-stack — essentially itself throwing the exception. This means that such an "intermediate method", gertSF() in our case, must also be annotated with the "throws clause".
Important! With a caveat that we will address shortly, any Java method that includes code that might result in an exception being thrown must either catch the exception, or have a
throws clause for that exception in its signature.
The answer to this is simple: derive different classes from Throwable that correspond to different kinds of errors. In fact, the API already has a whole class hierarchy rooted from Throwable and, of course, as we create our own exception classes we expand that hierarchy. To understand exception handling in Java, we really need to understand at least a part of that hierarchy.
Throwable
/ \
/ \
Error Exception
/ \
/ \
/ \
RuntimeException IOException
What's important here is that the compiler does not require
methods to handle (i.e. either catch or re-throw) Error's or
RuntimeException's. Everything else must be handled!
How does a hierarchy like this solve our other two problems? Based on the type of exception we catch we can determine categories of errors that occurred. Using polymorphism, different Throwable sub-types, can present us with information specific to their sub-type, but through the uniform interface of methods available in type Throwable.
If we run this, we get output like the following:
~/$ java Ex2 Error! An argument is required. ~/$ java Ex2 1,df Error! Argument included a non-integer value. $ java Ex2 1,0 Bad stuff happened! ~/$ java Ex2 , 0 ~/$ java Ex2 -1 Bad stuff happened!So we've got some errors covered with nice explanantions. Others, not so much. Next class we'll use polymorphism and we'll define our own exceptions, all in the name of handling our errors nicely!