Exceptions
Exceptions are Java's error signaling system. Specifically, Exceptions indicate a run-time error as opposed to a compile-time error.
Compile-time errors indicate a problem with code that prevents it from compiling. See if you can find the error in the following snippet that will be caught by javac:
String test = "teststring"; Float f = 1.0f;
Exceptions are caused by run-time errors. They are generally caused when the syntax for a command is correct, but the command is functioning on invalid data. See if you can find the run-time error in the following snippet:
int[] iarray = new int[10];
for (int i=0; i<=10; i++) {
iarray[i] = i;
}
The above code throws a java.lang.ArrayIndexOutOfBoundsException. This is a long name for an error, but very descriptive. It will walk you onto the problem more quickly than the 'SegFault' from C.
This error checking system in Java is extremely powerful, and is capable of doing far more than you might expect. It turns out that Exceptions allow us to both announce and, equally importantly, recover from, errors. These errors can be of the I-can't-do-that sort (ie, NullPointerExceptions or "ArithmaticException: div by zero"). It also allows us to throw Exceptions when the cpu can physically handle a data value (i.e. a temperature of -400 deg Celsius) but that value makes no sense in the context of real-world data.
First examples
Compile the following code. See how many different ways you can make it crash:
| Example code with un-caught exceptions |
|---|
public class Example1 {
public static void main(String[] args) {
int int1 = Integer.parseInt(args[0]);
int int2 = Integer.parseInt(args[1]);
System.out.println(int1/int2);
}
}
|
Here is the same code with basic Exception handling:
| One Exception to catch all errors: |
|---|
public class Example2 {
public static void main(String[] args) {
try {
int int1 = Integer.parseInt(args[0]);
int int2 = Integer.parseInt(args[1]);
System.out.println(int1/int2);
} catch (Exception e) {
System.out.println("Unexpected input. Exiting.");
}
}
}
|
Here is the same code with more complete Exception handling:
| Multiple Exceptions to handle different errors: |
|---|
public class Example3 {
public static void main(String[] args) {
try {
int int1 = Integer.parseInt(args[0]);
int int2 = Integer.parseInt(args[1]);
System.out.println(int1/int2);
} catch (NumberFormatException e) {
System.out.println("Unable to parse user input to an integer.");
System.out.println(e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("You must pass two integers on the command line.");
} catch (ArithmeticException e) {
System.out.println("Illegal arithmetic procedure requested:");
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.println("Unexpected Exception caught. Exiting:");
System.out.println(e.getMessage());
} finally {
System.out.println("This line will always print.");
}
}
}
|
Reasons for throwing Exceptions
There are two main situations where we use Exceptions:
- Errors with external input. We cannot control the users or the file system. If we have a program that divides two user-supplied numbers, we cannot tell the user not to enter a zero for the divisor. The user will likely enter a zero just to see what the program will do. (I assure you that your instructors always test your code that way. I keep entering data until I can figure out how to break something.) If a program has to read a file named "config.ini", the program cannot control whether the file exists. The Exception block gives the program a graceful way of handling unexpected errors like these.
- Logic errors with our own code. It is your job as a programmer to get rid of these, but sometimes they happen. You can use Exceptions as a way to track down faulty code more quickly so that you can fix bugs that would otherwise go undetected.
The exceptions we've encountered so far have been thrown by the interpreter itself, like NullPointerExceptions. However, we can also have our code throw them ourselves, and there are good and valid reasons to do that.
In the course of this class, we've already encountered several circumstances in which we wanted to send a signal that some function has ended in some unusual way. For example, consider our Queue class. It is possible for the user to ask to peek() at an empty Queue. The peek() method had to return something - possibly an empty string, or possibly a "Sentinal" value. (Also called a "Flag".) The purpose of a Sentinal is to let the calling method know that an exceptional event has occured. Consider two different ways of implementing Queue.peek() below:
| public String peek() { return lastNode.getString(); } | public String peek() { if(lastNode) { return lastNode.getString(); } return "--EMPTY-QUEUE--"; } |
The first method does not check whether there are any Nodes in the Queue. If you used this method in your Lab, you would get a NullPointerException every time you ran peek() on an empty list of items. (NOTE - I tested this on your labs. More than half of them crashed.)
The second method returns a unique string: "--EMPTY-QUEUE--" if the list is empty. This is much better - the calling method can check whether the string returned from peek() matches this sentinal value. If it matches, then the calling method could print a helpful message like "There are no items in the Queue." Is there a downside to this method? What do you think would happen if you tried to push the phrase "--EMPTY-QUEUE--" onto the Queue? You would probably break your program's logic. Since pop() would check for the same string, you would probably never be able to retrieve the items in the Queue.
The first method above assumes that the user is never going to enter a bad request. Never make that assumption. You should always assume that people will push the wrong buttons or enter the wrong data into your program. Many people will do this maliciouly to see if they can break it. The main reason we use Exceptions is to prevent users from causing our programs to crash - intentionally or otherwise.
The problem with the second method above is that the code has has only one communications channel (the returned String) to talk to the calling method. It has to use the same channel to return the Node's string and pass the empty-Queue message. This can easily cause confusion. As we saw above, it limits the number of messages that can be passed.
We would be better off using a second communications channel to send the error message. How can we do that with our current set of tools? One option is to return a custom data class from peek() that contains two items. See the code below:
| public class Answer { public boolean hasString; public String data; public Answer (boolean b, String d) { hasString = b; data = d; } } ... public Answer peek() { if(lastNode) { return new Answer(true, lastNode.getString()); } return new Answer(false, ""); } |
The above system will work, but it is cumbersome. There are two data channels: 'hasString' and 'data'. The calling method needs to check hasString, and act according if it is true or false.
Can you imagine how complex your program would get if you had to do something like this to check every possible error? Your code would quickly balloon and be difficult to read. And you still might have crashes if there was an error case that you did not consider.
A second reason that you might want to throw an Exception is to make a loud announcement that something has gone wrong. For example, remember the method in our Rotor class that takes in a char, and returns an int saying what index that char is in?
public int charAt(char c) {
for (int i = 0; i < charArr.length; i++)
if (c==charArr[i])
return i;
return -1; //This line should never run!
}
My comment notes that if that char c is at all appropriate for our Rotor, that return -1 line should never run. If it were to run, it means something has gone wrong that we need to fix, and this message needs to be as loud and clear as possible!
Types of Exceptions
Java has many of these built in, all of which are subclasses of the Exception class. If you look at this inheritance diagram which I have pulled from Oracle's Java tutorial, you'll see there are two types of Exceptions; those that inherit from RuntimeException, and those that do not.

Throwing Exceptions
Consider our Queue. We might modify our peek() function to look like this:
public String peek() {
if (first != null)
return first.data; // Return the data in the first Node
throw new IndexOutOfBoundsException("The Queue is empty, and cannot be
peeked");
}
A couple things to note about this example: first, the Exception isn't returned, it's thrown (that's why it doesn't have to match the return type of String), and second, Exceptions are objects that are built by calling their constructor. You can build them either with a String message, or not. Just as with return statements, a throw statement ends the method immediately.
Also, consider our charAt function:
public int charAt(char c) {
for (int i = 0; i < charArr.length; i++)
if (c==charArr[i])
return i;
throw new IllegalArgumentException(c + " is not a valid input!");
}
Catching Exceptions
Throwing errors does us only a little bit of good if our program can't recover from errors. Fortunately, Java lets us "catch" the Exceptions that have been thrown, and respond to them. We do this using a try-catch-finally block:
try {
//Some code that may result in a thrown Exception
} catch (ATypeOfException atoe) {
//Code that runs if an ATypeOfException was thrown in the Try block
} catch (AnotherTypeException ate) {
//Code that runs if an AnotherTypeException was thrown in the Try block
} finally {
//Code that always runs when the try block exits
}
When an exception is thrown, the method involved either needs to have a catch block for that exception, or else it raises the exception to its own calling method, which can go all the way back to main(). Any parent method can catch the exception.
'Checked' vs. 'Unchecked' Exceptions
- Exceptions inherited from RuntimeException are 'Unchecked'
- All others are 'Checked'
- the phrase 'checked' means that the compiler checks that the exeption is caught or thrown by each affected method.
Any method that throws a Checked Exception must explicitly declare so:
public int charAt(char c) throws NoSuchAttributeException {
for (int i = 0; i < charArr.length; i++)
if (c==charArr[i])
return i;
throw new NoSuchAttributeException(c + " is not a valid input!");
}
Any method calling charAt() would have to either catch that Exception, or itself be labelled as throwing a NoSuchAttributeException.
Our previous examples used IndexOutOfBoundsException and IllegalArgumentException, which are both RuntimeExceptions. So, no explicit statement of "this method throws an exception" has been necessary.
Why have both Checked and Unchecked exceptions? This is yet another controversial topic:
- Checked Exceptions are more likely to be caused by something outside the programmer's control (e.g. FileNotFoundException)
- Unchecked Exceptions can happen almost anywhere - such as ArithmaticException: / by zero. It would be ugly to have to catch or throw this in every method.
Making our own Exceptions
...is really easy. Decide if you want your Exception to be a RuntimeException or not. (Unchecked or Checked). Inherit. Below is the full definition of a YouDidSomethingDumbException:
public class YouDidSomethingDumbException extends RuntimeException {
public YouDidSomethingDumbException() { super(); }
public YouDidSomethingDumbException(String msg) { super(msg); }
}
Now you can throw and catch YDSDExceptions just like any other.
Files
So far in this course, we haven't been reading or writing to files. That's because there are many things that can go wrong in dealing with files; the file might not exist, I/O might fail, any number of things. All of these things throw Exceptions, which must be caught! Until we knew about try-catch blocks, we couldn't do this. What do you think this code does?
Scanner in = new Scanner(System.in);
System.out.println("Please enter a filename to be read");
String fn = in.next();
try{
Scanner fin = new Scanner(new File(fn));
while (fin.hasNext()){
String word = fin.next();
System.out.print(word + " ");
}
} catch (FileNotFoundException fnfe) {
System.out.println(fn + " not found!");
}
For writing to files, we'll be using PrintWriters:
System.out.print("Enter a filename: ");
Scanner in = new Scanner(System.in);
PrintWriter pw;
try {
pw = new PrintWriter(new File(in.next()));
pw.println("Words!");
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
} finally {
pw.close();
}
Using 'assert' to throw Exceptions for testing
"The best errors are the ones that bring your code screeching to a halt"
Who said this? Every programmer who has ever had to debug a transient error. Finding a bug is the hardest part of fixing it. They are easiest to find when they halt the program.
The 'assert' command throws a special Exception if passed a boolean value that is not true.
Consider what would happen if you used the following code for testing your Temperature lab:
public static void main(String[] args) {
Fahrenheit f = new Fahrenheit(32.0d);
assert (f.toCelsius() == 0.0d);
}
If the temperature conversion is incorrect then the assert command throws an 'AssertionError'. The traceback tells you where the program stopped and lets you debug it. You could have hundreds of asserts in a test method. If they all pass, then you know that your library is running correctly. This is easier than reading all the output strings.
In Java 1.5 and up, you need to use the '-ea' flag (Enable Assertions) to use assert:
java -ea MyTestClass