Exceptions
EXCEPTIONS
When a run-time error occurs in Java, the JVM throws an exception,
prints an error message, and quits. Oddly, an exception is a Java object (named Exception
), and you can prevent the error message from printing and the program from terminating by catching
the Exception
that Java threw.
Purpose #1: Coping with Errors
Exceptions are a way of coping with unexpected errors. By catching exceptions, you can recover. For instance you try to open a file that doesn't exist or that you aren't allowed to read, Java will throw an exception. You can catch the exception, handle it, and continue, instead of letting the program crash.
1 | try { |
What does this code do?
- It executes the code inside the
try
braces. - If the
try
code executes normally, we skip over thecatch
clauses. - If the
try
code throws an exception, Java does not finish thetry
code. It jumps directly to the firstcatch
clause that matches the exception, and executes thatcatch
clause. By "matches", I mean that the actual exception object thrown is the same class as, or a subclass of, the static type listed in the "catch" clause.
When the "catch" clause finishes executing, Java jumps to the next line of code immediately after all the "catch" clauses.
The code within a "catch" clause is called an exception handler.
If the FileInputStream
constructor fails to find the file, it will throw a FileNotFoundException
. The line i = f.read()
is not executed; execution jumps directly to the first exception handler.
FileNotFoundException
is a subclass of IOException
, so the exception matches both "catch" clauses. However, only one "catch" clause is executed--the first one that matches. The second "catch" clause would exec the first were not present.
Note that you don't need a "catch" clause for every exception that can occur. You can catch some exceptions and let others propagate.
Purpose #2: Escaping a Sinking Ship
Believe it or not, you might want to throw your own exception. Exceptions are the easiest way to move program execution out of a method whose purpose has been defeated.
For example, suppose you're writing a parser that reads Java code and analyzes its syntactic structure. Parsers are quite complicated, and use many recursive calls and loops. Suppose that your parser is executing a method many methods deep within the program stack within many levels of loop nesting. Suddenly, your parser unexpectedly reaches the end of the file, because a student accidentally erased the last 50 lines of his program.
It's quite painful to write code that elegantly retraces its way back up through the method calls and loops when a surprise happens deep within a parser. A better solution? Throw an exception! You can even roll your own.
1 | public class ParserException extends Exception { } // distinguishable from other types of exceptions |
This class doesn't have any methods except the default constructor. There's no need; the only purpose of a ParserException
is to be distinguishable from other types of exceptions. Now we can write some parser methods.
1 | public ParseTree parseExpression() throws ParserException { // We have to declare it might throw a parserException. |
The throw
statement throws a ParserException
, thereby immediately getting us out of the routine. How is this different from a return
statement? First, we don't have to return anything. Second, an exception can propagate several stack frames down the stack, not just one, as we'll see shortly.
The method signature has the modifier throws ParserException
. This is necessary; Java won't let you compile the method without it. throws
clauses help you and the compiler keep track of which exceptions can propagate where.
1 | public ParseTree parse() throws ParserException, DumbCodeException { |
The parse()
method above shows how to define a method that can throw two (or more) exceptions. Since every exception is a subclass of Exception, we could have replaced the two exceptions with "Exception", but then the caller would have to catch all types of Exceptions. We don't want (in this case) to catch NullPointerExceptions or otherwise hide our bugs from ourselves.
When parseExpression()
throws an exception, it propagates right through the calling method parse()
and down to compile()
, where it is caught. compile()
doesn't need a throws ParserException
clause because it catches any ParserException
that can occur. In this code, the catch
clauses don't do anything except stop the exceptions.
If an exception propagates all the way out of main()
without being caught, the JVM prints an error message and halts. You've seen this happen many times.
Checked and Unchecked Throwables
The top-level class of things you can throw
and catch
is called Throwable. Here's part of the Throwable class hierarchy.
An Error generally represents a fatal error, like running out of memory or stack space. Failed assert
statements also generate a subclass of Error called an AssertionError
. Although you can throw or catch any kind of Throwable, catching an Error is rarely appropriate.
Most Exceptions, unlike Errors, signify problems you could conceivably recover from. The subclass RunTimeException
is made up of exceptions that might be thrown by the Java Virtual Machine, such as NullPointerException
, ArrayIndexOutOfBoundsException
, and ClassCastException
.
There are two types of Throwables. Unchecked Throwables are those a method can throw without declaring them in a "throws" clause. All Errors and RunTimeExceptions (including all their subclasses) are unchecked, because almost every method can generate them inadvertently, and it would be si we had to declare them.
All Exceptions except RunTimeExceptions are checked, which means t your method might throw one without catching it, it must declare that possibility in a "throws" clause. Examples of checked exceptions include IOException and almost any Throwable subclass you would make yourself. // If you extend, it will become unchecked. Not recommanded.
When a method calls another method that can throw a checked exception, it has just two choices.
- It can catch the exception, or
- it must be declared so that it "throws" the same exception itself.
The easiest way to figure out which exceptions to declare is to declare none and let the compiler's error messages tell you. (This won't work on the exams, though.)
The "finally" keyword
A finally clause can also be added to a "try."
1 | FileInputStream f = new FileInputStream("filename"); |
If the try
statement begins to execute, the finally
clause will be executed at the end, no matter what happens. finally
clauses are used to do things that need to be done in both normal and exceptional circumstances. In this example, it is used to close a file.
If statementX causes no exception, then the "finally" clause is executed, and 1 is returned.
If statementX causes a IOException
, the exception is caught, the catch
clause is executed, and then the "finally" clause is executed. After the "finally" clause is done, 2 is returned.
If statementX causes some other class of exception, the "finally" clause is executed immediately, then the exception continues to propagate down the stack.
In the example above, we've invoked the method printStackTrace
on the exception we caught. When an exception is constructed, it takes a snapshot of the stack, which can be printed later.
It is possible for an exception to occur in a catch
or finally
clause. An exception thrown in a "catch" clause will terminate the "catch" clause, but the "finally" clause will still get executed before the exception goes on. An exception thrown in a "finally" clause replaces the old exception, and terminates the "finally" clause and the method immediately.
However...you can nest a try
clause inside a catch
or finally
clause, thereby catching those exceptions as well.
Exception constructors
By convention, most Throwables
(including Exceptions) have two constructors. One takes no parameters, and one takes an error message in the form of a String.
1 | class MyException extends Exception { |
The error message will be prin it propagates out of main(), and it can be read by the Throwable.getMessage()
method. The constructors usually call the superclass constructors, which are defined in Throwable.
GENERICS
Suppose you're using a list of Objects to store Strings. When you fetch a String from the list, you have to cast it back to type String
before you can call the methods exclusive to Strings. If somehow an object that's not a String got into your list, the cast will throw an exception. It would be nice to have the compiler enforce the restriction that nothing but Strings can ever get into your list in the first place, so you can sleep at night knowing that your family is safe from a ClassCastException.
So Java offers generics, which allow you to declare general classes that produce specialized objects. For example, you can create an SList for Strings only, and another SList for Integers only, even though you only wrote one SList class. To specify the class, SList takes a type parameter.
1 | class SListNode<T> { // T is the formal parameter. |
You can now create and use an SList of Strings as follows.
1 | SList<String> l = new SList<String>(); // String is the actual parameter. |
Likewise, you can create an SList of Integers by using "SList
What are the advantages of generics?
- First, the compiler will ensure at compile-time that nothing but Strings can ever enter your SList
. - Second, you don't have to cast the Objects coming out of your SList back to Strings, so there is no chance of an unexpected
ClassCastException
at run time. If some bug in your program is trying to put Integer objects into your SList, it's much easier to diagnose the compiler refusing to put an Integer into anSList<String>
than it is to diagnose aClassCastException
occurring when you remove an Integer from a regular SList and try to cast it to String.
Generics are a complicated subject. Consider this to be a taste of them; hardly a thorough treatment. A good tutorial is available at https://www.seas.upenn.edu/˜cis1xx/resources/generics-tutorial.pdf .
Although Java generics are superficially similar to C++ templates, there's a crucial difference between them. In the example above, Java compiles bytecode for only a single SList class. This SList bytecode can be used by all different object types. It is the compiler, not the bytecode itself, that enforces the fact that a particular SList object can only store objects of a particular class. Conversely, C++ recompiles the SList methods for every type that you instantiate SLists on. The C++ disadvantage is that one class might turn into a lot of machine code. The C++ advantages are that you can use primitive types, and you get code optimized for each type. Java generics don't work with primitive types.
FIELD SHADOWING
Just as methods can be overridden in subclasses, fields can be shadowed
in subclasses. However, shadowing works quite differently from overriding. Whereas the choice of methods is dictated by the dyanamic type of an object, the choice of fields is dictated by the static type of a variable or object.
1 | class Super { |
Any object of class Sub now has two fields called x, each of which store a different integer. How do we know which field is accessed when we refer to x? It depends on the static type of the expression whose x field is accessed.
1 | Sub sub = new Sub(); |
1 | i = supe.x; // 2 |
The last four statements all use the same object, but yield different results. Recall that method overriding does not work the same way. Since both variables reference a Sub, the method Sub.f always overrides Super.f.
1 | i = supe.f(); // 4 |
W the variable whose shadowed field you want to access is "this"? You can cast "this" too, but a simpler alternative is to replace "this" with "super".
1 | class Sub extends Super { |
Whereas method overriding is a powerful benefit of object orientation, field shadowing is largely a nuisance. Whenever possible, avoid having fields in subclasses whose names are the same as fields in their superclasses.
Static methods can be shadowed too; they follow the same shadowing rules as fields. This might seem confusing: why do ordinary, non-static methods use one system (overriding) while static methods use an entirely different system (shadowing)? The reason is because overriding requires dynamic method lookup. Dynamic method lookup looks up the dynamic type of an object. A static method is not called on an object, so there's nothing whose dynamic type we can look up. Therefore, static methods can't use dynamic method lookup or overriding. So they use shadowing instead.
Static method shadowing, like field shadowing, is largely a nuisance.
"final" METHODS AND CLASSES
A method can be declared "final" to prevent subclasses from overriding it. Any attempt to override it will cause a compile-time error.
A class can be declared "final" to prevent it from being extended. Any attempt to declare a subclass will cause a compile-time error.
The only reason to declare a method or class "final" is to improve the speed of a program. The compiler can speed up method calls that cannot be overridden.