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:
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.LinkageError
IncompatibleClassChangeError
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.
package io.airbrake;
import java.util.GregorianCalendar;
public class Test {
public static void main(String[] args) {
try {
PaperbackBook paperbackBook = new PaperbackBook(
"The Stand",
"Stephen King",
1153,
new GregorianCalendar(1978, 0, 1).getTime()
);
System.out.println(paperbackBook.getTagline());
} catch (IncompatibleClassChangeError error) {
// Output expected IncompatibleClassChangeErrors.
System.out.println(String.format("[EXPECTED] %s", error.toString()));
error.printStackTrace();
} catch (Exception | Error throwable) {
// Output unexpected Exceptions/Errors.
System.out.println(String.format("[UNEXPECTED] %s", throwable.toString()));
throwable.printStackTrace();
}
}
}
package io.airbrake;
import java.util.Date;
public class BaseBook {
private String author;
private Integer pageCount;
private Date publishedAt;
private String title;/**
* Constructs a basic book.
*
* @param title Book title.
* @param author Book author.
*/
public BaseBook(String title, String author) {
this.author = author;
this.title = title;
}/**
* Constructs a basic book, with page count.
*
* @param title Book title.
* @param author Book author.
* @param pageCount Book page count.
*/
public BaseBook(String title, String author, Integer pageCount) {
this.author = author;
this.title = title;
this.pageCount = pageCount;
}/**
* Constructs a basic book, with page count, publication date, and publication type.
*
* @param title Book title.
* @param author Book author.
* @param pageCount Book page count.
* @param publishedAt Book publication date.
*/
public BaseBook(String title, String author, Integer pageCount, Date publishedAt) {
this.author = author;
this.title = title;
this.pageCount = pageCount;
this.publishedAt = publishedAt;
}/**
* Get author of book.
*
* @return Author name.
*/
public String getAuthor() {
return author;
}/**
* Get page count of book.
*
* @return Page count.
*/
public Integer getPageCount() {
return pageCount;
}/**
* Get published date of book.
*
* @return Published date.
*/
public Date getPublishedAt() {
return publishedAt;
}/**
* Get title of book.
*
* @return Title.
*/
public String getTitle() {
return title;
}/**
* Set author of book.
*
* @param author Author name.
*/
public void setAuthor(String author) {
this.author = author;
}/**
* Set page count of book.
*
* @param pageCount Page count.
*/
public void setPageCount(Integer pageCount) {
this.pageCount = pageCount;
}/**
* Set published date of book.
*
* @param publishedAt Page count.
*/
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
/**
* Set title of book.
*
* @param title Title.
*/
public void setTitle(String title) {
this.title = title;
}
}
package io.airbrake;
import java.text.DateFormat;
import java.util.Date;public class PaperbackBook extends BaseBook {
public PaperbackBook(String title, String author) {
super(title, author);
}public PaperbackBook(String title, String author, Integer pageCount) {
super(title, author, pageCount);
}public PaperbackBook(String title, String author, Integer pageCount, Date publishedAt) {
super(title, author, pageCount, publishedAt);
}
/**
* Get a formatted tagline with author, title, and page count.
*
* @return Formatted tagline.
*/
public String getTagline() {
return String.format("'%s' by %s is %d pages, in PAPERBACK format, and published %s.",
getTitle(),
getAuthor(),
getPageCount(),
DateFormat.getDateInstance().format(getPublishedAt()));
}
}
// Logging.java
package io.airbrake.utility;import java.sql.Timestamp;
import java.util.Arrays;import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.builder.*;/**
* Houses all logging methods for various debug outputs.
*/
public class Logging {
private static final char separatorCharacterDefault = '-';
private static final String separatorInsertDefault = "";
private static final int separatorLengthDefault = 40;/**
* Get a String of passed char of passed length size.
* @param character Character to repeat.
* @param length Length of string.
* @return Created string.
*/
private static String getRepeatedCharString(char character, int length) {
// Create new character array of proper length.
char[] characters = new char[length];
// Fill each array element with character.
Arrays.fill(characters, character);
// Return generated string.
return new String(characters);
}/**
* Outputs any kind of Object.
* Uses ReflectionToStringBuilder from Apache commons-lang library.
*
* @param value Object to be output.
*/
public static void log(Object value)
{
if (value == null) return;
// If primitive or wrapper object, directly output.
if (ClassUtils.isPrimitiveOrWrapper(value.getClass()))
{
System.out.println(value);
}
else
{
// For complex objects, use reflection builder output.
System.out.println(new ReflectionToStringBuilder(value, ToStringStyle.MULTI_LINE_STYLE).toString());
}
}/**
* Outputs any kind of String.
*
* @param value String to be output.
*/
public static void log(String value)
{
if (value == null) return;
System.out.println(value);
}/**
* Outputs any kind of String, with prefixed timestamp.
*
* @param value String to be output.
* @param includeTimestamp Indicates if timestamp should be included.
*/
public static void log(String value, boolean includeTimestamp)
{
if (value == null) return;
if (includeTimestamp) {
System.out.println(String.format("[%s] %s", new Timestamp(System.currentTimeMillis()), value));
} else {
log(value);
}
}/**
* Outputs passed in Throwable exception or error instance.
* Can be overloaded if expected parameter should be specified.
*
* @param throwable Throwable instance to output.
*/
public static void log(Throwable throwable)
{
// Invoke call with default expected value.
log(throwable, true);
}/**
* Outputs passed in Throwable exception or error instance.
* Includes Throwable class type, message, stack trace, and expectation status.
*
* @param throwable Throwable instance to output.
* @param expected Determines if this Throwable was expected or not.
*/
public static void log(Throwable throwable, boolean expected)
{
System.out.println(String.format("[%s] %s", expected ? "EXPECTED" : "UNEXPECTED", throwable.toString()));
throwable.printStackTrace();
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator() {
lineSeparator(separatorInsertDefault, separatorLengthDefault, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert) {
lineSeparator(insert, separatorLengthDefault, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(int length) {
lineSeparator(separatorInsertDefault, length, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(int length, char separator) {
lineSeparator(separatorInsertDefault, length, separator);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(char separator) {
lineSeparator(separatorInsertDefault, separatorLengthDefault, separator);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert, int length) {
lineSeparator(insert, length, separatorCharacterDefault);
}/**
* See: lineSeparator(String, int, char)
*/
public static void lineSeparator(String insert, char separator) {
lineSeparator(insert, separatorLengthDefault, separator);
}/**
* Outputs a dashed line separator with
* inserted text centered in the middle.
*
* @param insert Inserted text to be centered.
* @param length Length of line to be output.
* @param separator Separator character.
*/
public static void lineSeparator(String insert, int length, char separator)
{
// Default output to insert.
String output = insert;if (insert.length() == 0) {
output = getRepeatedCharString(separator, length);
} else if (insert.length() < length) {
// Update length based on insert length, less a space for margin.
length -= (insert.length() + 2);
// Halve the length and floor left side.
int left = (int) Math.floor(length / 2);
int right = left;
// If odd number, add dropped remainder to right side.
if (length % 2 != 0) right += 1;// Surround insert with separators.
output = String.format("%s %s %s", getRepeatedCharString(separator, left), insert, getRepeatedCharString(separator, right));
}
System.out.println(output);
}
}
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 tostatic
. - A
non-constant
field is changed tonon-static
. - A
class
is changed to aninterface
. - Or, an
interface
is changed to aclass
.
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:
package io.airbrake;
import java.util.Date;
public class BaseBook {
private String author;
private Integer pageCount;
private Date publishedAt;
private String title;/**
* Constructs a basic book.
*
* @param title Book title.
* @param author Book author.
*/
public BaseBook(String title, String author) {
this.author = author;
this.title = title;
}/**
* Constructs a basic book, with page count.
*
* @param title Book title.
* @param author Book author.
* @param pageCount Book page count.
*/
public BaseBook(String title, String author, Integer pageCount) {
this.author = author;
this.title = title;
this.pageCount = pageCount;
}/**
* Constructs a basic book, with page count, publication date, and publication type.
*
* @param title Book title.
* @param author Book author.
* @param pageCount Book page count.
* @param publishedAt Book publication date.
*/
public BaseBook(String title, String author, Integer pageCount, Date publishedAt) {
this.author = author;
this.title = title;
this.pageCount = pageCount;
this.publishedAt = publishedAt;
}/**
* Get author of book.
*
* @return Author name.
*/
public String getAuthor() {
return author;
}/**
* Get page count of book.
*
* @return Page count.
*/
public Integer getPageCount() {
return pageCount;
}/**
* Get published date of book.
*
* @return Published date.
*/
public Date getPublishedAt() {
return publishedAt;
}/**
* Get title of book.
*
* @return Title.
*/
public String getTitle() {
return title;
}/**
* Set author of book.
*
* @param author Author name.
*/
public void setAuthor(String author) {
this.author = author;
}/**
* Set page count of book.
*
* @param pageCount Page count.
*/
public void setPageCount(Integer pageCount) {
this.pageCount = pageCount;
}/**
* Set published date of book.
*
* @param publishedAt Page count.
*/
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
/**
* Set title of book.
*
* @param title Title.
*/
public void setTitle(String title) {
this.title = title;
}
}
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
:
package io.airbrake;
import java.text.DateFormat;
import java.util.Date;public class PaperbackBook extends BaseBook {
public PaperbackBook(String title, String author) {
super(title, author);
}public PaperbackBook(String title, String author, Integer pageCount) {
super(title, author, pageCount);
}public PaperbackBook(String title, String author, Integer pageCount, Date publishedAt) {
super(title, author, pageCount, publishedAt);
}
/**
* Get a formatted tagline with author, title, and page count.
*
* @return Formatted tagline.
*/
public String getTagline() {
return String.format("'%s' by %s is %d pages, in PAPERBACK format, and published %s.",
getTitle(),
getAuthor(),
getPageCount(),
DateFormat.getDateInstance().format(getPublishedAt()));
}
}
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:
package io.airbrake;
import java.util.GregorianCalendar;
public class Test {
public static void main(String[] args) {
try {
PaperbackBook paperbackBook = new PaperbackBook(
"The Stand",
"Stephen King",
1153,
new GregorianCalendar(1978, 0, 1).getTime()
);
System.out.println(paperbackBook.getTagline());
} catch (IncompatibleClassChangeError error) {
// Output expected IncompatibleClassChangeErrors.
System.out.println(String.format("[EXPECTED] %s", error.toString()));
error.printStackTrace();
} catch (Exception | Error throwable) {
// Output unexpected Exceptions/Errors.
System.out.println(String.format("[UNEXPECTED] %s", throwable.toString()));
throwable.printStackTrace();
}
}
}
Executing the main(String[] args)
method works as expected, outputting the generated tagline
of our book:
'The Stand' by Stephen King is 1153 pages, in PAPERBACK format, and published Jan 1, 1978.
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
:
public class BaseBook {
// ...private static String title;
// ...
/**
* Get title of book.
*
* @return Title.
*/
public static String getTitle() {
return title;
}// ...
}
We want to save our changes in the BaseBook
binary so we need to recompile it:
$ javac io/airbrake/BaseBook.java
With the title
field now set to static
let's execute the Test.main(String[] args)
method a second time and see what happens:
$ java io.airbrake.Test
[EXPECTED] java.lang.IncompatibleClassChangeError: Expecting non-static method io.airbrake.PaperbackBook.getTitle()Ljava/lang/String;
at io.airbrake.PaperbackBook.getTagline(PaperbackBook.java:26)
at io.airbrake.Test.main(Test.java:14)
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! Try Airbrake free for 30 days.