picture home | pixelblog | qt_tools

omino code blog

We need code. Lots of code.
David Van Brink // Fri 2007.05.25 19:31 // {java}

Java Source versus Binary Compatibility

This post shall be just a short note on an observed Java behavior… I learned it by breaking something.

Part of my current job involves creating and maintaining a couple of public API’s. We’re still working out all the rules and policy as to what, exactly, must be compatible with what. For now, each release of our product includes a full install, so we can, in principle, change anything we want as long as it’s all self consistent.

In practice, however, various individuals and groups are writing code against these API’s, and may or may not be enthusiastic to change their code based on my whimsies. But at least their code gets rebuilt automatically.

In the future, we expect third parties to write plugins to our product against our API’s. When they start doing that, we’ll have to be both API and binary backwards-compatible. Or at least consider carefully who we will alienate if we break their plugins.

Ok. So consider the following simple API. Let’s call this our “platform”, and it consists of three Java classes, like so.

package sa.api;
public class Animal{/**/}
package sa.api;
public class AquaticAnimal extends Animal{/**/}
package sa.api;
public class Utils
{
    public static Animal getIt() { return null; }
}

Next, consider some client code. I’ve called it a “plugin” but really, it’s any code that you expect to link against the “platform” above.

package sa.plugin;
import sa.api.Animal;
import sa.api.Utils;
public class Plugin
{
    public static Animal getAnimal()
    {
        Animal a = Utils.getIt();
        return a;
    }
}

All very simple. This code works just fine. Now here’s the fun part. We’re doing Rev 2 of our API, and we realize that in every case of Utils.getIt(), as it turns out, we’re returning not just an Animal, but an AquaticAnimal. So, we change the method return value, like so:

package sa.api;
public class Utils
{
    // We used to return Animal, 
    // but since it's assignable,
    // we think this change is safe.
    // dvb07, rev2
    public static AquaticAnimal getIt() 
        { return null; }
}

Sourcecode-wise, this change is fine! Our existing line Animal a = Utils.getIt(); still compiles, since AquaticAnimal “is a” Animal. It’s all legal.

But.

Our old copy of Plugin.jar, the one that was built against the original version, has problems. You see, the compiled code stores the whole signature of its methods including return types! When we load our old jar against the rev-2 platform, we get the following error:

java.lang.NoSuchMethodError: sa.api.Utils.getIt()Lsa/api/Animal;

Conclusions
Recreating the above error in a controlled situation, to write this entry, was a bit tricky! Initially I had Animal and AquaticAnimal as static inner classes of Utils, and I couldn’t get the error to happen. And creating this error in isolation requires a bit of handiness with Jar building and executing. I did it all in Eclipse with a supercharged class loader, but it was still tricky to induce.

If you need to maintain binary compatibility, you can think hard about it, try to do things which seem “safe”… but the only way you’ll really know is if you keep around actual compiled code and test it against your revised API regularly.

Significantly: Checking against source code is not sufficient! The source code gets some benefit during recompilation.

It seems so obvious, but there y’have it. Back to the debugging, go I!

1 comments
Douglas // Sun 2007.06.10 07:467:46 am

I believe this is why the Java class library writers deprecate APIs and write new methods that parallel old ones.

oh, i dont know. what do you think?



(c) 2003-2011 omino.com / contact poly@omino.com