Exceptions
Table of Contents
Introduction
General purpose programming languages provide an immense amount of freedom for the programmer. Assuming hardware can keep up, absolutely anything that can be imagined can be created. From programs that simulate the building blocks of the universe, to programs that render Sonic the Hedgehog into three dimensional space, a general purpose programming language is only limited by the creativity and engineering capabilities of the programmer. This freedom is a double-edged sword, however, as it also provides the programmer with more than enough rope to hang herself with. Some languages, like Java, provide built-in mechanisms for dealing with situations where these sorts of issues arise. In Java, these mechanisms are called exceptions and they allow for the programmer to plan against potential errors that might crop up during program execution.
What is an Exception?
A definition taken directly from the language specification (§ 11) should do nicely:
When a program violates the semantic constraints of the Java programming language, the Java Virtual Machine signals this error to the program as an exception.
At this point, we know that a "semantic constraint" refers to anything in the language that must be satisfied to keep up with the meaning and use of the language. For example, dividing a number by zero would "violate the semantic constrains of the Java programming language" because Java explicitly does not allow for such a thing to happen. Let's check it out:
1: public class DivideByZero{ 2: public static void main(String[] args){ 3: System.out.printf("5/0 = %d%n", 5/0); 4: } 5: }
After compiling and running we get:
josephraskind@stargazer:/tmp/exceptions$ javac DivideByZero.java; java DivideByZero
Exception in thread "main" java.lang.ArithmeticException: / by zero
at DivideByZero.main(DivideByZero.java:3)
The program compiles just fine, but when we try to run it we get an exception at the exact moment the illegal operation is performed. Again, this happens to prevent such illegal semantic occurrences from being processed by the runtime engine. Java does not define what happens when an integer is divided by zero and as such nothing should happen.
Not all languages provide built-in exceptions. For example, C categorizes nearly all semantic violations as "undefined behavior" where the language designers functionally ignore edge cases.
Types of Exceptions
Java defines two sorts of exceptions: checked exceptions and unchecked exceptions.
Unchecked Exceptions
The ArithmeticException exception we witnessed earlier falls under the unchecked exception category. Unchecked exceptions are all exceptions that are either subclasses of RuntimeException or Error. RuntimeException is a category of exception whereby recovery is thought to still be possible whereas for Error it is unlikely the program can recover. What separates unchecked exceptions from checked exceptions si that they do not need to be wrapped in a try-catch clause—neither do they need the throws modifier appended to a method declaration.
Checked Exceptions
Checked exceptions are all exceptions which are not unchecked exceptions. By contrast, checked exceptions must follow a strict set of syntax in order to function properly:
- Checked exceptions must be caught using a try-catch clause; or
- Checked exceptions must be included in a method declaration annotation
For example, the standard library class FileReader's constructor will throw a FileNotFoundException if an incorrect filepath was sent into the method (you can refer to the method API here):
1: import java.io.FileReader; 2: public class UsingFileReader{ 3: public static void main(String[] args){ 4: FileReader fr = new FileReader("./fake_file.txt"); 5: } 6: }
When I compile the above code I get the following:
josephraskind@stargazer:/tmp/exceptions$ java UsingFileReader.java
UsingFileReader.java:5: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader fr = new FileReader("./fake_file.txt");
^
1 error
error: compilation failed
Because FileReader throws a FileNotFoundException I must either tag the main with a throws annotation, or I must catch the exception in a try-catch block:
1: import java.io.FileReader; 2: import java.io.FileNotFoundException; 3: public class UsingFileReader{ 4: public static void main(String[] args) throws FileNotFoundException{ 5: FileReader fr = new FileReader("./fake_file.txt"); 6: } 7: }
Now I can compile and run it:
josephraskind@stargazer:/tmp/exceptions$ java UsingFileReader.java
Exception in thread "main" java.io.FileNotFoundException: ./fake_file.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at UsingFileReader.main(UsingFileReader.java:5)
Perfect!
Now let's see how it looks with the try-catch statement:
1: import java.io.FileReader; 2: import java.io.FileNotFoundException; 3: public class UsingFileReader{ 4: public static void main(String[] args){ 5: try{ 6: FileReader fr = new FileReader("./fake_file.txt"); 7: }catch(FileNotFoundException e){ 8: e.printStackTrace(); 9: } 10: } 11: }
After compiling and running we get:
josephraskind@stargazer:/tmp/exceptions$ java UsingFileReader.java
java.io.FileNotFoundException: ./fake_file.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at UsingFileReader.main(UsingFileReader.java:6)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.execute(SourceLauncher.java:264)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.run(SourceLauncher.java:153)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.main(SourceLauncher.java:78)
Handling Exceptions
Now we have some idea of what exceptions are, how do they work? Simply put, when an exception is thrown control will be transferred to the "nearest" catch block in the method call stack. If none is found then excecution will be halted. The following diagram visualizes the process:
method_c() throws Exception
+---------------------------+
| int x = 1 / 0; | ← Exception originates here
| // ArithmeticException |
+---------------------------+
|
| exception propagates up
| (method_c has no try/catch)
v
method_b()
+---------------------------+
| method_c(); | ← Exception passes through
| // no try/catch | method_b unhandled
+---------------------------+
|
| exception continues propagating
v
method_a()
+------------------------------------------+
| try{ |
| method_b(); | ← Exception caught here
| } catch(ArithmeticException e){ |
| System.out.println("Caught: " + e); |
| } |
+------------------------------------------+
|
| execution continues normally
v
main()
+---------------------------+
| method_a(); | ← Returns here after
| // continues normally | exception was handled
+---------------------------+
Call stack at time of exception:
+---------------------------+ ← top of stack (exception origin)
| method_c() |
+---------------------------+
| method_b() |
+---------------------------+
| method_a() | ← exception caught here
+---------------------------+
| main() |
+---------------------------+ ← bottom of stack
The try-catch statement will catch on exceptions of a given class. For example, in the above diagram the catch is set to ArithmeticException so it can only match with an ArithmeticException or a subclass of ArithmeticException. We can define try-catch statements with more than one possible catch:
1: public class MultipleCatches{ 2: public static void main(String[] args){ 3: int user = Integer.parseInt(args[0]); 4: try{ 5: int res = 1 / user; // Will throw exception if user is 0 6: int[] arr = new int[1]; 7: int val = arr[user]; // Will throw exception if user > 0 8: }catch(ArithmeticException e){ 9: System.out.println("Caught by ArithmeticException catch!"); 10: }catch(RuntimeException e){ 11: System.out.println("Caught by RuntimeException catch!"); 12: } 13: System.out.println("Exiting..."); 14: } 15: }
If we compile and run the code with two different inputs:
josephraskind@stargazer:/tmp/exceptions$ java MultipleCatches.java 0 Caught by ArithmeticException catch! Exiting... josephraskind@stargazer:/tmp/exceptions$ java MultipleCatches.java 1 Caught by RuntimeException catch! Exiting...
First, let's take a look at what happened in the initial run. We sent in "0" as the input which was converted to the integer 0 and used to divide 1. This cannot happen which causes Java to throw an ArithmeticException. Because the statement was wrapped in a try-catch block control will tried to be transferred to the catch block. The first matching block is on Line 8 as it matches the exception exactly. This causes the "Caught by ArithmeticException catch!" to be printed out to the screen.
Second, on the latter run we sent in "1" as the input which was converted to the integer 1 and used to divide 1. This does not cause any errors which allows for the array, arr, to be instantiated. We then try to retrieve the value at index 1 which does not exist and therefore throws an IndexOutOfBoundsException. There is no explicit catch for IndexOutOfBoundsException present, but there is for RuntimeException of which IndexOutOfBoundsException is a subclass. Therefore, the exception is caught by the catch block on Line 10 which causes the "Caught by RuntimeException catch!" to be printed out to the screen.
Additionally, since both exceptions were caught by the try-catch block the program exited normally as evidenced by the two "Exiting..." printouts. This is the major advantage of exception handling. Other languages may have the entire runtime system crash when such an event occurs, but Java allows for these mistakes to be dealt with to prevent such a serious occurrance.
Defining an Exception
Java provides us with the ability to define our own exceptions. Sometimes the exceptions provided by the Java standard library do not reflect the sorts of issues that might crop up in our own program logic. Let's take the BankAccount class as an example. We know that it is unreasonable to consider a call to withdraw which has a negative amount—you cannot take negative money out of the bank. This provides us with a perfect opportunity to define an exception that we can raise any time a withdrawl or transation occurs! We can do so as follows:
1: public class WithdrawlException extends RuntimeException{ 2: public WithdrawlException(String msg){ 3: super(msg); 4: } 5: }
I defined a new class caled WithdrawlException which is a subclass of RuntimeException. I then provide a constructor which takes a message to be printed out by whatever catches the exception. We can alter the BankAccount class to throw a WithdrawlException in the event of a negative withdrawl:
1: public class BankAccount { 2: private String owner; 3: private double balance; 4: 5: BankAccount(String owner, double initialBalance){ 6: this.owner = owner; 7: this.balance = initialBalance; 8: } 9: 10: void deposit(double amount){ 11: balance = balance + amount; 12: } 13: 14: void withdraw(double amount){ 15: if(amount < 0){ 16: throw new WithdrawlException("Cannot have a negative amount!"); 17: } 18: balance = balance - amount; 19: } 20: 21: void printBalance(){ 22: System.out.printf("%s's balance: $%.2f%n", owner, balance); 23: } 24: }
When the illegal operation is found (a negative amount is sent to withdraw) the WithdrawlException is thrown. Throw is a reserved keyword that always takes an exception object—because one is not in memory previously I must instantiate it with a call to new. Finally, we can test it out with a main class:
1: public class NegativeWithdrawl{ 2: public static void main(String[] args){ 3: BankAccount acc = new BankAccount("Alice", 1000); 4: acc.withdraw(-1000); 5: System.out.println("Exiting..."); 6: } 7: }
After compiling and running we get:
josephraskind@stargazer:/tmp/exceptions$ java NegativeWithdrawl.java
Exception in thread "main" WithdrawlException: Cannot have a negative amount!
at BankAccount.withdraw(BankAccount.java:16)
at NegativeWithdrawl.main(NegativeWithdrawl.java:4)
Voilá! After sending in a negative amount to withdraw in the main function we set off a chain of events which lead to the termination of the application.
The "Exiting…" string was not printed out to the screen. This is because the WithdrawlException was not caught and therefore it was propagated "outside" of the main
leading to the program halting before the println.
Exercises
- Compile and run Listing 1. Then wrap the division in a try-catch block that catches
ArithmeticExceptionand prints a friendly error message instead of crashing. Verify that execution continues normally after the catch block by adding aSystem.out.println("Program continues")after the try-catch. - The chapter shows two ways to handle a checked exception: using
throwsas in Listing 5 and using try-catch as in Listing 7. Implement both approaches yourself usingFileReader. In your own words explain the difference in behavior between the two — what happens to the exception in each case? - Compile and run Listing 9 with the inputs
0and1to verify the results match the chapter. Then run it with the input-1. What happens? Trace through the code manually and explain which exception is thrown and which catch block handles it. - The chapter states that catch blocks are matched in order from top to bottom. Write a program with a try block that throws an
ArithmeticExceptionand two catch blocks: one forRuntimeExceptionand one forArithmeticException, in that order. Does it compile? What does the compiler tell you and why? - Add a
WithdrawlExceptioncatch block to Listing 13 so that the program handles the exception gracefully and prints"Exiting..."as originally intended. Verify that the program no longer crashes and that the message is printed. The chapter defines
WithdrawlExceptionas a subclass ofRuntimeExceptionmaking it an unchecked exception. Redefine it as a subclass ofExceptioninstead to make it a checked exception:public class WithdrawlException extends Exception { public WithdrawlException(String msg){ super(msg); } }
Recompile
BankAccountandNegativeWithdrawl. What new compiler errors appear? What changes must you make to get the code to compile again? What does this tell you about the practical difference between checked and unchecked exceptions?- Add a second custom exception
OverdraftExceptiontoBankAccountthat is thrown when a withdrawal would cause the balance to go below zero. Test it by attempting to withdraw more than the available balance. Then write amainthat catches bothWithdrawlExceptionandOverdraftExceptionseparately and prints a different message for each. - The propagation diagram in the chapter shows an exception passing through
method_bbefore being caught inmethod_a. Implement this exact scenario in Java with three methods calling each other. Verify the output matches the diagram. Then add a try-catch insidemethod_bas well — does the exception still reachmethod_a? - The chapter notes that
"Exiting..."was not printed in Listing 14 because the uncaught exception halted the program. Java provides afinallyblock that executes regardless of whether an exception was thrown or caught. Researchfinallyand add one to Listing 13 that always prints"Exiting...". Verify it prints even when the exception is not caught. - Write a class
SafeCalculatorwith a static methoddivide(int a, int b)that returns the result ofa / bbut throws a customDivisionByZeroException(which you must define) whenbis zero. Write amainthat callsdividewith several inputs including zero, catches the exception, and continues running after each call. MakeDivisionByZeroExceptionan unchecked exception and explain why that is the appropriate choice here.