Backward Compatibility Caveats

It’s a universal goal — to attempt to make the most of your software. Backward compatibility is one tool that helps, and it is not at all foreign to the Java Platform development effort. A JDK 1.5 compiler is able to produce classes meant to run in a Java 1.4 Virtual Machine, as part of the promise of compatibility. The Java 1.5 JVM can run code written for Java 1.4. In order for all this to work, the core libraries must retain some level of backward compatibility.

A Java library lives through the classes, interfaces, methods, fields — the functionality that it advertises, its contract. When speaking of the evolution of a library, you must speak of what you add, remove and modify in its contract.

To deal with the “remove clause”, the designers of the Java platform created deprecation as a built-in annotation feature, which marks fields, methods or entire classes and means “to be removed in a future version; please consider not using me anymore”. This gives clients of the library time to adapt to the change.

As far as “modify” goes, Java has been good at maintaining functionality intact, sometimes at the cost of cluttering a class. (See java.lang.Vector, retrofitted to expose the java.util.List interface of the Java Collection Framework.) They’ve refined the specification of a certain function here and there, and even changed the semantics of certain components, when deemed buggy (java.util.Calendar being notorious here), but they stayed pretty clear of making them modifications in the long run.

The “add” part, on the other hand, has proven to be a source of various compatibility issues.

Example 1: Class A in your code implements an interface B located in a core Java library. When you compile/run your code with a newer JDK/JVM, which has added methods to interface B, you can’t compile/run your code on the newer JDK/JVM.

Example 2: Class X in your code extends a class Y located in a core Java library. Class Y is not abstract, so there’s no risk of future abstract methods causing incompatibilties. Your class X also has a private (or protected) method m. In a newer Java version, class Y may contain a public method m with the same signature, which will cause compiler error “attempting to assign weaker privileges to method m” in well behaved compilers.

Example 3: This more subtle incompatibility happened at my workplace. A colleague noticed a java.lang.NoSuchMethodError having to do with an usage of java.math.BigDecimal. The culprit was this statement: new java.math.BigDecimal(0).

The java.lang.NoSuchMethodError exception was caused by compiling the class using a JDK 1.5 compiler (in our automated build system), and then running the bytecode in a Java 1.4 Virtual Machine. This problem was not occurring in development, where we compile and run with a Java 1.4 Virtual Machine.

So what happened here? The Java 1.4 java.math.BigDecimal class does not have a constructor that takes an argument of type int, but it does have a constructor that takes an argument of type double. The development compiler coerces the argument to type double, and, when writing the class, it makes a note to call the constructor with one argument of type double upon execution of the statement. When the virtual machine executes the statement, it locates the right constructor of class java.math.BigDecimal and invokes it. In the automated build system, however, we invoked the JDK 1.5 compiler, which uses the JDK 1.5 libraries for linkage. As it turns out, the Java 1.5 java.math.BigDecimal class has an additional constructor, which takes an argument of type int. The JDK 1.5 compiler makes a note to call the constructor that takes an int when this line gets executed. When the Java 1.4 Virtual Machine executes code from our class, it uses its own libraries, including its version of java.math.BigDecimal. When instructed to invoke the constructor that takes an int, it can’t find one, and must throw a java.lang.NoSuchMethodError. Of course, this is because type coercion does not function at this level. And it should not either, because then you couldn’t use the overloaded method feature.

In this case, adding functionality (a new constructor) to an existing non-abstract class is what breaks compatibility. And you may have noticed that the problem in example 3 extends to any platform that has implicit type conversion features.

Post a Comment

You must be logged in to post a comment.