To better understand the concept, it is interesting to see that binary compatibility does NOT imply API compatibility, nor vice versa.
API compatible but NOT binary compatible: static removal
Version 1 of library:
public class Lib {
public static final int i = 1;
}
Client code:
public class Main {
public static void main(String[] args) {
if ((new Lib()).i != 1) throw null;
}
}
Compile client code with version 1:
javac Main.java
Replace version 1 with version 2: remove static
:
public class Lib {
public final int i = 1;
}
Recompile just version 2, not the client code, and run java Main
:
javac Lib.java
java Main
We get:
Exception in thread "main" java.lang.IncompatibleClassChangeError: Expected static field Lib.i
at Main.main(Main.java:3)
This happens because even though we can write (new Lib()).i
in Java for both static
and member methods, it compiles to two different VM instructions depending on Lib
: getstatic
or getfield
. This break is mentioned at JLS 7 13.4.10:
If a field that is not declared private was not declared static and is changed to be declared static, or vice versa, then a linkage error, specifically an IncompatibleClassChangeError, will result if the field is used by a pre-existing binary which expected a field of the other kind.
We would need to recompile Main
with javac Main.java
for it to work with the new version.
Notes:
- calling static members from class instances like
(new Lib()).i
is bad style, raises a warning, and should never be done
- this example is contrived because non-static
final
primitives are useless: always use static final
for primitives: private final static attribute vs private final attribute
- reflection could be used to see the difference. But reflection can also see private fields, which obviously leads to breaks which were not supposed to count as breaks, so it does not count.
Binary compatible but NOT API compatible: null pre-condition strengthening
Version 1:
public class Lib {
/** o can be null */
public static void method(Object o) {
if (o != null) o.hashCode();
}
}
Version 2:
public class Lib {
/** o cannot be null */
public static void method(Object o) {
o.hashCode();
}
}
Client:
public class Main {
public static void main(String[] args) {
Lib.method(null);
}
}
This time, even if recompile Main
after updating Lib
, the second invocation will throw, but not the first.
This is because we changed the contract of method
in a way that is not checkable at compile time by Java: before it could take null
, after not anymore.
Notes:
- the Eclipse wiki is a great source for this: https://wiki.eclipse.org/Evolving_Java-based_APIs
- making APIs that accept
null
values is a questionable practice
- it is much easier to make a change that breaks API compatibility but not binary than vice versa, since it is easy to change the internal logic of methods
C binary compatibility example
What is an application binary interface (ABI)?