Chapter 7
Error Handling
Error handling is an important feature in any programming language. A good error handling mechanism makes it easier for programmers to write robust applications and to prevent bugs from creeping in. In some languages, programmers are forced to use multiple if statements to detect all possible conditions that might lead to an error. This could make code excessively complex. In a larger program, this practice could easily lead to spaghetti like code.
Java has a very nice approach to error handling by using the try statement. With this strategy, part of the code that could potentially lead to an error is isolated in a block. Should an error occur, this error is caught and resolved locally. This chapter teaches you how.
Catching Exceptions
You can isolate code that may cause a runtime error using the try statement, which normally is accompanied by the catch and finally statements. Such isolation typically occurs in a method body. If an error is encountered, Java stops the processing of the try block and jump to the catch block. Here you can gracefully handle the error or notify the user by ‘throwing’ ajava.lang.Exception object. Another scenario is to re-throw the exception or a new Exception object back to the code that called the method. It is then up to the client how he or she would handle the error. If a thrown exception is not caught, the application will stop abruptly.
This is the syntax of the try statement.
try { [code that may throw an exception] } [catch (ExceptionType-1 e) { [code that is executed when ExceptionType-1 is thrown] }] [catch (ExceptionType-2 e) { [code that is executed when ExceptionType-2 is thrown] }] ... } [catch (ExceptionType-n e) { [code that is executed when ExceptionType-n is thrown] }] [finally { [code that runs regardless of whether an exception was thrown]] }]
The steps for error handling can be summarized as follows:
1. Isolate code that could lead to an error in the try block.
2. For each individual catch block, write code that is to be executed if an exception of that particular type occurs in the try block.
3. In the finally block, write code that will be run whether or not an error has occurred.
Note that the catch and finally blocks are optional, but one of them must exist. Therefore, you can have try with one or more catch blocks or try with finally.
The previous syntax shows that you can have more than one catch block. This is because the code can throw different types of exceptions. When an exception is thrown from a tryblock, control is passed to the first catch block. If the type of exception thrown matches or is a subclass of the exception in the first catch block, the code in the catch block is executed and then control goes to the finally block, if one exists.
If the type of the exception thrown does not match the exception type in the first catch block, the JVM goes to the next catch block and does the same thing until it finds a match. If no match is found, the exception object will be thrown to the method caller. If the caller does not put the offending code that calls the method in a try block, the program will crash.
To illustrate the use of this error handling, consider the NumberDoubler class in Listing 7.1. When the class is run, it will prompt you for input. You can type anything, including non-digits. If your input is successfully converted to a number, it will double it and print the result. If your input is invalid, the program will print an “Invalid input” message.
Listing 7.1: The NumberDoubler class
import java.util.Scanner; public class NumberDoubler { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String input = scanner.next(); try { double number = Double.parseDouble(input); System.out.printf("Result: %s", number); } catch (NumberFormatException e) { System.out.println("Invalid input."); } } }
The NumberDoubler class uses the java.util.Scanner class to take user input (Scanner was discussed in Chapter 5, “Core Classes”).
Scanner scanner = new Scanner(System.in); String input = scanner.next();
It then uses the static parseDouble method of the java.lang.Double class to convert the string input to a double. Note that the code that calls parseDouble resides in a try block. This is necessary because the parseDouble method may throw a java.lang.NumberFormatException, as indicated by the signature of the parseDouble method.
public static double parseDouble(String s) throws NumberFormatExcpetion
The throws statement in the method signature tells you that it could throw a NumberFormatException and it is the responsibility of the method caller to catch it.
Without the try block, invalid input will give you this embarrassing error message before the system crashes:
Exception in thread "main" java.lang.NumberFormatException:
try without catch
A try statement can be used with finally without a catch block. You normally use this syntax to ensure that some code always gets executed whether or not an unexpected exception has been thrown in the try block. For example, after opening a database connection, you want to make sure its close method is called after you're done with the connection. To illustrate this scenario, consider the following pseudocode that opens a database connection.
Connection connection = null; try { // open connection // do something with the connection and perform other tasks } finally { if (connection != null) { // close connection } }
If something unexpected occurs in the try block, the close method will always be called to release the resource.
Catching Multiple Exceptions
Java 7 and later allows you to catch multiple exceptions in a single catch block if the caught exceptions are to be handled by the same code. The syntax of the catch block is as follows, two exceptions being separated by the pipe character |.
catch(exception-1 | exception-2 ... e) { // handle exceptions }
For example, the java.net.ServerSocket class's accept method can throw four exceptions: java.nio.channels.IllegalBlockingModeException, java.net.SocketTimeoutException,java.lang.SecurityException, and java.io.Exception. If, say, the first three exceptions are to be handled by the same code, you can write your try block like this:
try { serverSocket.accept(); } catch (SocketTimeoutException | SecurityException | IllegalBlockingModeException e) { // handle exceptions } catch (IOException e) { // handle IOException }
The try-with-resources Statement
Many Java operations involve some kind of resource that has to be closed after use. Before JDK 7, you used finally to make sure a close method is guaranteed to be called:
try { // open resource } catch (Exception e) { } finally { // close resource }
This syntax can be tedious especially if the close method can throw an exception and can be null. For example, here's a typical code fragment to open a database connection.
Connection connection = null; try { // create connection and do something with it } catch (SQLException e) { } finally { if (connection != null) { try { connection.close(); } catch (SQLException e) { } } }
You see, you need quite a bit of code in the finally block just for one resource, and it's not uncommon to have to open multiple resources in a single try block. JDK 7 adds a new feature, the try-with-resource statement, to make resource closing automatic. Its syntax is as follows.
try ( resources ) { // do something with the resources } catch (Exception e) { // do something with e }
For example, here is opening a database connection would look like in Java 7.
Connection connection = null; try (Connection connection = openConnection(); // open other resources, if any) { // do something with connection } catch (SQLException e) { }
Not all resources can be automatically closed. Only resource classes that implement java.lang.AutoCloseable can be automatically closed. Fortunately, in JDK 7 many input/output and database resources have been modified to support this feature. You'll see more examples of try-with-resources in Chapter 13, “Input/Output” and Chapter 22, “Java Database Connectivity.”
The java.lang.Exception Class
Erroneous code can throw any type of exception. For example, an invalid argument may throw a java.lang.NumberFormatException, and calling a method on a null reference variable throws a java.lang.NullPointerException. All Java exception classes derive from the java.lang.Exception class. It is therefore worthwhile to spend some time examining this class.
Among others, the Exception class has the following methods:
public String toString()
Returns the description of the exception.
public void printStackTrace()
Prints the description of the exception followed by a stack trace for the Exception object. By analyzing the stack trace, you can find out which line is causing the problem. Here is an example of what printStackTrace may print on the console.
java.lang.NullPointerException at MathUtil.doubleNumber(MathUtil.java:45) at MyClass.performMath(MyClass.java: 18) at MyClass.main(MyClass.java: 90)
This tells you that a NullPointerException has been thrown. The line that throws the exception is Line 45 of the MathUtil.java class, inside the doubleNumber method. ThedoubleNumber method was called by MyClass.performMath, which in turns was called by MyClass.main.
Most of the time a try block is accompanied by a catch block that catches the java.lang.Exception in addition to other catch blocks. The catch block that catches Exception must appear last. If other catch blocks fail to catch the exception, the last catch will do that. Here is an example.
try { // code } catch (NumberFormatException e) { // handle NumberFormatException } catch (Exception e) { // handle other exceptions }
You may want to use multiple catch blocks in the code above because the statements in the try block may throw a java.lang.NumberFormatException or other type of exception. If the latter is thrown, it will be caught by the last catch block.
Be warned, though: The order of the catch blocks is important. You cannot, for example, put a catch block for handling java.lang.Exception before any other catch block. This is because the JVM tries to match the thrown exception with the argument of the catch blocks in the order of appearance. java.lang.Exception catches everything; therefore, the catch blocks after it would never be executed.
If you have several catch blocks and the exception type of one of the catch blocks is derived from the type of another catch block, make sure the more specific exception type appears first. For example, when trying to open a file, you need to catch the java.io.FileNotFoundException just in case the file cannot be found. However, you may want to make sure that you also catch java.io.IOException so that other I/O-related exceptions are caught. Since FileNotFoundException is a child class of IOException, the catch block that handlesFileNotFoundException must appear before the catch block that handles IOException.
Throwing an Exception from a Method
When catching an exception in a method, you have two options to handle the error that occurs inside the method. You can either handle the error in the method, thus quietly catching the exception without notifying the caller (this has been demonstrated in the previous examples), or you can throw the exception back to the caller and let the caller handle it. If you choose the second option, the calling code must catch the exception that is thrown back by the method.
Listing 7.2 presents a capitalize method that changes the first letter of a String to upper case.
Listing 7.2: The capitalize method
public String capitalize(String s) throws NullPointerException { if (s == null) { throw new NullPointerException( "Your passed a null argument"); } Character firstChar = s.charAt(0); String theRest = s.substring(1); return firstChar.toString().toUpperCase() + theRest; }
If you pass a null to capitalize, it will throw a new NullPointerException. Pay attention to the code that instantiates the NullPointerException class and throws the instance:
throw new NullPointerException( "Your passed a null argument");
The throw keyword is used to throw an exception. Don't confuse it with the throws statement which is used at the end of a method signature to indicate that an exception of a given type may be thrown from the method.
The following example shows code that calls capitalize.
String input = null; try { String capitalized = util.capitalize(input); System.out.println(capitalized); } catch (NullPointerException e) { System.out.println(e.toString()); }
Note
A constructor can also throw an exception.
User-Defined Exceptions
You can create a user-defined exception by subclassing java.lang.Exception. There are several reasons for having a user-defined exception. One of them is to create a customized error message.
For example, Listing 7.3 shows the AlreadyCapitalizedException class that derives from java.lang.Exception.
Listing 7.3: The AlreadyCapitalizedException class
package app07; public class AlreadyCapitalizedException extends Exception { public String toString() { return "Input has already been capitalized"; } }
You can throw an AlreadyCapitalizedException from the capitalize method in Listing 7.2. The modified capitalize method is given in Listing 7.4.
Listing 7.4: The modified capitalize method
public String capitalize(String s) throws NullPointerException, AlreadyCapitalizedException { if (s == null) { throw new NullPointerException( "Your passed a null argument"); } Character firstChar = s.charAt(0); if (Character.isUpperCase(firstChar)) { throw new AlreadyCapitalizedException(); } String theRest = s.substring(1); return firstChar.toString().toUpperCase() + theRest; }
Now, the capitalize method may throw one of two exceptions. You comma-delimit multiple exceptions in a method signature.
Clients that call capitalize must now catch both exceptions. This code shows a call to capitalize.
StringUtil util = new StringUtil(); String input = "Capitalize"; try { String capitalized = util.capitalize(input); System.out.println(capitalized); } catch (NullPointerException e) { System.out.println(e.toString()); } catch (AlreadyCapitalizedException e) { e.printStackTrace(); }
Since NullPointerException and AlreadyCapitalizedException do not have a parent-child relationship, the order of the catch blocks above is not important.
When a method throws multiple exceptions, rather than catch all the exceptions, you can simply write a catch block that handles java.lang.Exception. Rewriting the code above:
StringUtil util = new StringUtil(); String input = "Capitalize"; try { String capitalized = util.capitalize(input); System.out.println(capitalized); } catch (Exception e) { System.out.println(e.toString()); }
While it's more concise, the latter lacks specifics and does not allow you to handle each exception separately.
Final Words on Exception Handling
The try statement imposes some performance penalty. Therefore, do not use it over-generously. If it is not hard to test for a condition, then you should do the testing rather than depending on the try statement. For example, calling a method on a null object throws a NullPointerException. Therefore, you could always surround a method call with a try block:
try { ref.method(); ...
However, it is not hard at all to check if ref is null prior to calling methodA. Therefore, the following code is better because it eliminates the try block.
if (ref != null) { ref.methodA(); }
Summary
This chapter discussed the use of structured error handling and presented examples for each case. You have also been introduced to the java.lang.Exception class and its properties and methods. The chapter concluded with a discussion of user-defined exceptions.
Question
1. What is the advantage of the try statement?
2. Can a try statement be used with finally and without catch?
3. What is try-with-resources?
0 comments:
Post a Comment