Java Exceptions Explained: Origin, Purpose, and Mechanism (Part-1 of JAVA SE Exceptions Series)
Table of contents
- History
- Lisp, Ada, COBOL, and others
- Normal & Abnormal
- Return & Goto
- Exceptions & OOP
- Checked & Unchecked
- Original Intent
- Faults
- Contingency
- Raising an Exception
- Creating
- Throwing
- Table 1: Errors from the java.lang package
- Table 2: Runtime Exceptions from the java.lang package
- The try-catch Statements
- How Runtime Matches catch Blocks
TL;DR: Explore the evolution of Java exceptions from historical interrupts to modern handling techniques. Learn about types like StackOverflowError, OutOfMemoryError, and how to handle them effectively in your Java applications.
History
The notion of interrupts, introduced in mid 1950s, is not exactly the same as modern exception handling but is close to it. Interrupts refer to both hardware and software. In both cases there is a request to the processor to interrupt (or delay, or postpone – use any word you like) its current activity and handle this specific situation. After the specific situation is handled, the processor may resume its interrupted activity.
Lisp, Ada, COBOL, and others
Lisp was the first language that introduced software exception handling in a way similar to what we know today. It introduced key words like ERRSET, ERR, CATCH, and THROW. Their meanings and usages were similar to what they are today. Computer languages like Ada, COBOL, Object Pascal, and others quickly adopted the concept. Its implementation may vary from language to language, but the core idea is the same.
C/C++
C did not have exceptions. In 1985, C++ was released as an object-oriented extension of C. In addition to other things, exception handling was an integral part of the program. C++ added the power of object-oriented programming to it.
C++ is the language from which the architects of Java borrowed exception handling semantics. With C++, one may declare an exception in a method signature, but its usage will not be enforced by the compiler.
The architects of Java, as we will see later, tried to take things one step further. They added the idea of checked exceptions. This means that some exceptions may become a part of a method signature, thus requiring special handling.
Normal & Abnormal
The whole concept behind exception handling (and its conceptual predecessors) is that there are two ways that a program may be executed.
The program executes normally; all language elements work as they should; or
The program executes abnormally - there is not enough memory allocated, a null value is passed instead of a reference to a valid object, or there is no file by the location passed by the caller.
The separation of normal and abnormal execution sounds simple and intuitive, and normally does not cause controversy. However, some experts go further and support the idea of a separation of abnormal behavior further into two categories - “recoverable abnormal” and “unrecoverable abnormal”.
“Recoverable abnormal” conditions can be intercepted by the program itself and tried for recovery. “Unrecoverable abnormal” however would normally require termination of the execution and human intervention.
Return & Goto
Exceptions can be described as alternative return values, and exception handling as an alternative return mechanism. But, contrary to a return statement, they may return a call not to the next level up the call stack, but to an arbitrary level higher, bypassing all intermediate checks for the return value. So, another way to describe them is a non-local conditional goto.
Exceptions & OOP
Objects have very convenient semantics. In OOP languages (including Java) the information about exceptional behavior can be extracted, encapsulated, and separated from the normal flow. In addition to information about where, when, and how the exceptional conditions took place, information can be transparently added to the object that describes the exceptional condition. In summary, in OOP languages exceptions combine the power of non-local transition of control with object-oriented semantics
Types
Java is an object-oriented language, which means classes play an important role in it and its multiple concepts are represented in the form of classes.
Every exception is an object of a class with some specific behavior.
Classifying Exceptions
Java runtime always passes an Exception object to your exception handler. Because the types of exceptions or runtime errors that can occur in your application can be very large, it will be difficult to assimilate the information provided by a single Exception object. Therefore, the Exception class is categorized into several subclasses. the fig shows the high-level view of the Exception class hierarchy.
At the top of the class hierarchy you have the Throwable class. All other exception classes, including your own designed exception classes, inherit from the Throwable class. Both Error and Exception are subclasses of Throwable. The Error class denotes the fatal errors, and the Exception class denotes the non-fatal errors, discussed earlier. We will be focusing on the Exception class hierarchy, which has several subclasses, each meeting a specific situation. For example, the class ArrayIndexOutOfBoundsException is used for designating an illegal access to an array element, whereas the class ArithmeticException describes an exception that may occur during an arithmetic operation.
The purpose behind creating this exception hierarchy is to give you the option of treating various exception cases differently and to allow specialized information to reside in exception classes specific to particular situations.
Several subclasses of the Exception class are provided for this purpose so that you can catch a specific type of exception. For each specific type of exception you want to capture, you have to write a separate catch block. Thus, your code will consist of multiple catch blocks when you want to handle different types of exceptions differently. The order in which these blocks are defined in your code is also important. When the code inside a try block throws an exception, its catch clauses are examined in their order of appearance in the source file. Your program should first try to catch an exception of a subclass type.
If no such subclass exceptions are handled, eventually your code provides an exception handler for the most generic Exception class.
The syntax for incorporating this feature is shown here:
try {
blockStatementsopt
} catch ( ExceptionType1 exceptionObjectName1 ) {
blockStatementsopt
}
...
} catch ( ExceptionTypeN exceptionObjectNameN ) {
blockStatementsopt
}
A typical try with multiple catch blocks looks like this:
try {
// code that may generate a runtime exception
} catch (ReadOnlyBufferException e1) {
// your error handler
} catch (UnsupportedOperationException e2) {
// your error handler
} catch (Exception e) { // other catch blocks
// your error handler
}
Java SE 7 has added new syntax for catching multiple exception types in the same catch clause,
as shown here:
try {
// code that may generate a runtime exception
} catch (ReadOnlyBufferException | UnsupportedOperationException e1) {
// your error handler
}
When you use this syntax, you provide a common error handler for all these types of exceptions.
NOTE: A subclass exception must be handled before its superclass exception. If you write an Exception handler block as the first block in your multiple-exception-handler code, this would always get called whenever an exception occurs and the code provided in other exception handlers would never get called. The compiler catches this error and complains about “unreachable code.”
Checked & Unchecked
The three important subclasses (in the java.lang.* package) of the java.lang.Throwable in the exception hierarchy are: public class Error extends Throwable public class Exception extends Throwable public class RuntimeException extends Exception The three classes – java.lang.Error, java.lang.Exception and java.lang.RuntimeException – do not add anything to the programming logic as it is defined in the Throwable. They are “marker interfaces”, rather than fully functional classes. Subclasses of Error and RuntimeException are called unchecked exceptions. All other classes directly or indirectly inherited from Throwable are called checked exceptions. As we will see later, the Java compiler treats checked and unchecked exceptions, in some respects, very differently.
Original Intent
The exception classes structure, as stated by Sun/Oracle, was intended to divide two types of possible program failures, namely faults and contingencies
Faults
A “Fault” is something that should never happen. Examples include misconfiguration of program environment, JVM failures, software bugs and similar. Subclasses of java.lang.Error and java.lang.RuntimeException were reserved for this purpose.
Contingency
A “Contingency” is something that may be unpleasant but is expected and is even a part of a method return protocol. Examples include not enough funds, user not found, etc. These events should be expected, and the programming logic should be prepared to handle them. Other subclasses of java.lang.Throwable (except subclasses of java.lang.Error and java.lang.RuntimeException, mentioned in the previous paragraph) were reserved for this purpose.
Raising an Exception
There are two stages in raising an exception in Java. First, the exception should be created in the same way any other Java object is created. But simply creating an object of a Throwable class (or one of its subclasses) is not enough. Second, the exception should be put in a specific state. Specifically, it should be thrown. Some other languages use the word raised. In Java the special statement throw serves this purpose.
Creating
If an exception is initiated by the user, as with any other Java object, it should first be explicitly created. It may be one of the existing core Java exception classes. The user may also decide to create their own class and use an exception of the corresponding class. Therefore, the operator new should be called with one of the constructors available for the specific class: Exception exception = new Exception(); // new MyException()
Throwing
Creating an exception is the first step of initiating an exceptional situation. For the second step, the exception should be thrown; i.e., a special statement should be used with it. throw exception; Throwing an exception can be combined with the previous creation step: throw new Exception(); After an exception is thrown it begins its “flight” up the stack trace to the nearest handling block. If the JVM does not find an appropriate handler in the code, it will terminate the corresponding thread that has thrown the exception. Users do not have to create an exception every time from scratch. They may use an already available exception. This process is called re-throwing.
Two points should be made regarding re-throwing. First, if the reference used for it is null, then instead of your exception, an instance of java.lang.NullPointerException will be thrown. This happens because the JVM detects an attempt to throw a null exception object, which is not allowed. There are multiple situations when the JVM may decide to create and throw an exception of its own. Second, a re-thrown exception may already have some information in it, which is irrelevant to the new context. That information should possibly be reset. This will be covered in detail in later chapters.
Detection by the JVM As has already been mentioned, an exception may be created it by the JVM. It may detect an exceptional situation and take care of In this case the JVM will create an exception object (by calling an existing constructor) and throw it. The JVM creates and throws this way only unchecked exceptions (Error, RuntimeException, and their subclasses). According to the JVM specification [2] the following exceptions may be detected and thrown by the JVM.
Table 1: Errors from the java.lang
package
Error Type | Description |
StackOverflowError | Occurs when the stack is full, typically due to excessive recursion. |
AbstractMethodError | Thrown when an application tries to call an abstract method. |
AssertionError | Thrown when an assertion fails. |
ClassCircularityError | Occurs when a circularity is detected while initializing a class. |
ClassFormatError | Indicates that the Java virtual machine encountered an invalid class file format. |
ExceptionInInitializerError | Thrown when an exception occurs during the evaluation of a static initializer. |
IllegalAccessError | Indicates that access to a class or member is not allowed. |
IncompatibleClassChangeError | Thrown when a class file has a version number that is not supported. |
InstantiationError | Thrown when an application tries to create an instance of a class using the new keyword, but the class object cannot be instantiated. |
LinkageError | Indicates a generic problem with the class linkage. |
NoClassDefFoundError | Thrown if the Java Virtual Machine tries to load a class file and cannot find the requested class. |
NoSuchFieldError | Thrown if an application tries to access or modify a specified field of an object, and that field does not exist. |
OutOfMemoryError | Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory. |
UnsatisfiedLinkError | Thrown when the Java Virtual Machine cannot find an appropriate native-language definition of a method declared native . |
VerifyError | Thrown when the class file fails verification by the Java Virtual Machine. |
VirtualMachineError | A superclass for errors that occur within the Java Virtual Machine. |
Table 2: Runtime Exceptions from the java.lang
package
Runtime Exception Type | Description |
ArithmeticException | Thrown when an exceptional arithmetic condition has occurred. |
ArrayIndexOutOfBoundsException | Thrown when an array has been accessed with an illegal index. |
ArrayStoreException | Thrown to indicate that an attempt has been made to store the wrong type of object into an array of objects. |
ClassCastException | Thrown when an application tries to cast an object to a subclass of which it is not an instance. |
IllegalArgumentException | Thrown when a method has been passed an illegal or inappropriate argument. |
IllegalMonitorStateException | Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor. |
InstantiationException | Thrown when an application tries to create an instance of a class using the newInstance method in Class , but the specified class object cannot be instantiated. |
InterruptedException | Thrown when a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted, either before or during the activity. |
NegativeArraySizeException | Thrown when an application tries to create an array with a negative size. |
NullPointerException | Thrown when an application tries to use null in a case where an object is required. |
These tables categorize the errors and runtime exceptions based on their types and provide a brief description of each.
The try-catch Statements
To catch such exception conditions in your program, Java provides a construct called the try-catch block. The susceptible code that may generate an exception at runtime is enclosed in a try block, and the exception-handling code is enclosed in a catch block. The syntax of a try-catch block is shown here:
try {
blockStatementsopt
} catch ( ExceptionType exceptionObjectName ) {
blockStatementsopt
}
A try-catch block in your program code looks like this:
try {
// code that may generate a runtime or some kind of exception
} catch (Exception e) {
// your error handler
}
If an exception occurs while the code in the try block is being executed, the Java runtime creates an object of the Exception class, encapsulating the information on what went wrong, and transfers the program control to the first statement enclosed in the catch block. The code in the catch block analyzes the information stored in the Exception object and takes the appropriate corrective action. The exception in the previous example can be handled gracefully by putting the susceptible printVisitorList method in a try-catch block, as shown here:
try {
roster.printVisitorList();
} catch (Exception e) {
System.out.println("Quitting on end of list");
}
Now, when you run the program, you will get a more graceful quit message on the terminal at the end of the list. The partial output is shown here:
Visitor ID # gc051hh3hba7
Visitor ID # i98ivsnwunp4
Visitor ID # rapll6ouc0m6
Quitting on end of list
Unfortunately, this error message appears on the terminal in any iteration of the while loop if an error occurs in that iteration, and not necessarily only at the end of the list.
How Runtime Matches catch Blocks
A thrown exception object is caught by the catch block that specifies the class of the occurred exception or its superclass.
In the case of multiple catch blocks, these are evaluated sequentially in the order they are specified by applying the first rule. If a catch block is found, the rest of the catch blocks are ignored.
A certain catch block will never be executed if a catch block containing its superclass is listed prior to it. In such situations, a compile-time error is generated.
The compiler forces the programmer to handle all checked exceptions. In other words, you must provide error handling for all exceptions except for the RuntimeException and its subclasses.
If the try block never throws an exception specified in the catch list, the compiler generates an error.
Related official doc links:
https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html
Related official explainer links:
https://docs.oracle.com/javase/specs/jls/se13/html/jls-11.html
https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html
Subscribe to my newsletter
Read articles from Asesh Basu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by