It’s pretty easy to break binary and source compatibility simultaneously. Remove one parameter from a public method or delete a method entirely and no existing code that used the method will compile or run. But how can we break just binary or just source compatibility but not both? Let’s look at examples of each in Java.

Source Compatibility

This methods prints out every item in a collection.

public void printAllElements(Collection<?> collection)
{
  for (Object each : collection)
  {
    System.out.println(each);
  }
}

The method takes a Collection as its parameter, but it could really operate on any Iterable. So let’s make that change:

public void printAllElements(Iterable<?> iterable)
{
  for (Object each : iterable)
  {
    System.out.println(each);
  }
}

This change is source compatible, meaning all source that compiled against the previous version of this code will still compile. That makes sense, because every Collection is an Iterable, so every existing reference to this method is still valid.

This change is not binary compatible though. If you swap out the old jar for the new one without recompiling, you’ll get an error at runtime like this:

Exception in thread “main” java.lang.NoSuchMethodError: com.motlin.Printer.printAllElements(Ljava/util/Collection;)V

The bytecode of the client application refers to this method very specifically. You can see the method signature in the error message, and the type of the parameter is part of the signature. The method com.motlin.Printer.printAllElements(Ljava/util/Collection;)V no longer exists. To use the new library, client code must be recompiled. That might not sound so bad, but transitive dependencies need to be recompiled as well. That means if the client application uses a second framework which depends on the first, the second framework will also need to be recompiled. In a large dependency graph, the extra work can add up.

Binary Compatibility

How can we break source compatibility while preserving binary compatibility? One way is to add a method to an interface. Let’s write a tiny library with one interface and one concrete implementation. Here’s the complete source of version 1.

public interface MyInterface
{
  void doSomething();
}

public class MyImpl implements MyInterface
{
  public void doSomething()
  {
    System.out.println("");
  }
}

Version 2 adds a second method to MyInterface called doSomethingElse(). That’s not a source compatible change.  MyInterface is public, which means any client can write their own implementations of it. When clients upgrade, their implementations won’t implement doSomethingElse() yet so they’ll get a compiler error like this:

com.motlin.MyImpl is not abstract and does not override abstract method doSomethingElse() in com.motlin.MyInterface

The rules for binary compatibility are quite complex and you can read the rules elsewhere. The reason why this change is binary compatible is analogous to why the previous change wasn’t. A client application compiled against version 1 has references in the bytecode to the method doSomething() which is void and takes no parameters. That method is still there in version 2, so it will work, simple as that. Internally, doSomething() can call doSomethingElse() in version 2 and it would still be ok. The internal implementation of a method does not change its signature, so everything is still fine. Binary compatibility is very dynamic. It is determined at runtime and difficult to reason about earlier.

 
  • Oleksandr Gavenko

    I also use runtime and compile time terms for binary/source compatibility…

    • http://motlin.com Craig P. Motlin

      They do roughly correspond, but some changes overlap. For example, deleting a public method breaks both source and binary compatibility.