In Java, each exception belongs to one of the following category:
unchecked (run-time) exceptions – those classes that have java.lang.RuntimeException as one of the base classes;
checked exceptions – those classes that have java.lang.Exception as one of the base classes, but not java.lang.RuntimeException.
Notice that java.lang.RuntimeException extends java.lang.Exception as a base class. It means that to catch all exceptions, it is enough to have one catch-block that catches java.lang.Exception:
void catchRuntimeException(){
try{
m1(42);
} catch (Exception ex){
ex.printStackTrace();
}
}
void m1(int i){
throw new RuntimeException();
}
In the above code, the catch-block of the method catchRuntimeException() will catch the exception thrown by the method m1(). Naturally, the following code also works just fine:
void catchRuntimeException(){
try{
m1(42);
} catch (RuntimeException ex){
ex.printStackTrace();
}
}
Checked and unchecked (run-time) exceptions
Now let us talk about the difference between run-time and checked exceptions: the compiler does not force you to catch RuntimeException, so the following code compiles without an error:
void dontCatchRuntimeException(){
m1(42);
}
By contrast, the following code does not compile:
void m2(int i){
throw new Exception();
}
The compiler forces you either to catch the checked exception or list it in the throws-clause:
void m2(42) throws Exception{
throw new Exception();
}
void catchException(){
try{
m2(42);
} catch (Exception ex){
ex.printStackTrace();
}
}
void throwsException() throws Exception{
m2(42);
}
The checked exceptions were introduced originally in order to provide an ability to identify an underlying problem based on the exception type:
class Problem1Exception extends Exception {}
class Problem2Exception extends Exception {}
void m3(int i) throws Problem1Exception, Problem2Exception{
if(i > 0){
throw new Problem1Exception();
} else if(i > 0){
throw new Problem2Exception();
}
// do what has to be done
}
void catchParticularProblemException(){
Random random = new Random();
int i = random.nextInt();
try{
m3(i);
} catch (Problem1Exception ex){
//do something to address Problem1
} catch (Problem2Exception ex){
//do something to address Problem2
}
}
The first software libraries tried to use this feature and threw a specific checked exception when the input data were invalid and under similar conditions. But pretty soon, Java programmers have realized that using the message to communicate the cause of the problem is much more practical. For example, let us re-write our last example using RuntimeException only:
void m4(int i) {
if(i > 0){
throw new RuntimeException("Problem1");
} else if(i > 0){
throw new RuntimeException("Problem2");
}
// do what has to be done
}
void catchRuntimeExceptionWithMessage(){
Random random = new Random();
int i = random.nextInt();
try{
m4(i);
} catch (RuntimeException ex){
if("Problem1".equals(ex.getMessage())){
//do something to address Problem1
} else if("Problem2".equals(ex.getMessage())){
//do something to address Problem2
} else {
throw new RuntimeException("Unexpected problem: " + ex.getMessage());
}
}
}
The above code provides the same functionality as the previous example but has an advantage of not requiring creating two classes of checked exceptions.
Unfortunately, this approach was not standardized (yet) and libraries authors either continue to throw checked exceptions or do not guarantee the message stability (so, the library user cannot rely on the same message spelling from one library
Better exception handling solution
In practice, the application developers do not write code that addresses a problem encountered by a library method. They rather prefer avoid the conditions that may cause the problem. For example, they check the input data and make sure they cannot cause a problem:
void avoidException(){
Random random = new Random();
int i = random.nextInt();
if(i != 0){
// do something to correct the problem
// or
throw new RuntimeException("Invalid input value = " + i);
}
m4(i);
}
All possible sources of bad data are eliminated during the application development.
Even the Java authors indirectly confirm the advantage of this approach. As of this writing, in the package java.lang, there are 15 run-time exceptions and only 9 checked exception. And among the checked exceptions, there are several that are not recoverable automatically (which was the rational behind the adding checked exception to Java in the first place), for example:
ClassNotFoundException is thrown when JVM cannot find a class;
CloneNotSupportedException is thrown when the object does not implement Cloneable interface;
IllegalAccessException is thrown when the currently executing code does not have access to the specified class, field, method, or constructor.
That is why the list of the recommendations for exception handling includes the following:
always catch all exceptions;
handle each exception as close to the source as possible;
if you need to re-throw an exception, convert the third-party checked and run-time exception into your application-specific run-time exception with the corresponding message;
do not use exceptions for driving the business logic.
Read more detailed discussion of this topic in the following books:
Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.