Java Exception Handling

Java Exception Handling – IncompatibleClassChangeError

Next up, in our deep dive into Java Exception Handling, today we’ll be digging into the IncompatibleClassChangeError. This error is a base exception class for a variety of errors thrown when the Java Virtual Machine (JVM) recognizes an incompatibility between two compiled class definitions that are executing in tandem. In child class, which we just looked at last week, is the AbstractMethodError, which extends the IncompatibleClassChangeError and is thrown when abstract method incompatibilities are detected between two classes.

In today’s article we’ll explore the IncompatibleClassChangeError by seeing where it resides in the overall Java Exception Hierarchy. We’ll also look at some functional sample code that illustrates a common scenario for class extension, and how a handful of incompatible changes can result in IncompatibleClassChangeErrors in your own code. Let’s get into it!

The Technical Rundown

All Java errors implement the java.lang.Throwable interface, or are extended from another inherited class therein. The full exception hierarchy of this error is:

Full Code Sample

Below is the full code sample we’ll be using in this article. It can be copied and pasted if you’d like to play with the code yourself and see how everything works.

When Should You Use It?

The appearance of an IncompatibleClassChangeError — or any child error therein — is a result of what is known as binary incompatibility in Java. There is a great article here detailing all the ways an API package will (and will not) break binary compatibility, but we can summarize the common scenarios below. Incompatibilities occur when an existing binary (i.e. compiled Java class) references a newly-modified binary, and that modified binary contains any one of a handful of potentially incompatible changes:

  • A non-final field is changed to static.
  • A non-constant field is changed to non-static.
  • A class is changed to an interface.
  • Or, an interface is changed to a class.

These four modifications are the primary way an IncompatibleClassChangeError can be thrown. To illustrate, our simple example uses two similar classes to handle book object creation, starting with the BaseBook class:

BaseBook contains four private properties and associated getter/setter methods for title, author, pageCount, and publishedAt. For our use case we want to create a handful of extension classes based on BaseBook, which will be used to create specific types of book publications. One such extension class is PaperbackBook:

For this example we’re not adding much to the PaperbackBook class, save for the new getTagline() method. To make sure a PaperbackBook object inherits everything from BaseBook that we need we’ll test it in the Test.main(String[] args) method:

Executing the main(String[] args) method works as expected, outputting the generated tagline of our book:

Cool! We now have binary compatibility between the compiled BaseBook and PaperbackBook classes. However, what happens if we start to modify the BaseBook class definition and recompile it, without also recompiling the PaperbackBook class? As we saw above, the first of the four possible ways to cause binary incompatibility is to change a non-final field to static. Therefore, let’s change the BaseBook.getTitle() method (and associated private String title field) to static:

We want to save our changes in the BaseBook binary so we need to recompile it:

With the title field now set to static let’s execute the Test.main(String[] args) method a second time and see what happens:

As you probably expected, we’re now catching an IncompatibleClassChangeError, which indicates that PaperbackBook.getTitle() (which is, of course, extended from BaseBook.getTitle()) should’ve been non-static. In such scenarios, the obvious solution is to either use an IDE that will catch such incompatibilities before runtime, or to ensure you always recompile all associated classes simultaneously, even when the source code of a single class is modified.

The Airbrake-Java library provides real-time error monitoring and automatic exception reporting for all your Java-based projects. Tight integration with Airbrake’s state of the art web dashboard ensures that Airbrake-Java gives you round-the-clock status updates on your application’s health and error rates. Airbrake-Java easily integrates with all the latest Java frameworks and platforms like Spring, Maven, log4j, Struts, Kotlin, Grails, Groovy, and many more. Plus, Airbrake-Java allows you to easily customize exception parameters and gives you full, configurable filter capabilities so you only gather the errors that matter most.

Check out all the amazing features Airbrake-Java has to offer and see for yourself why so many of the world’s best engineering teams are using Airbrake to revolutionize their exception handling practices!