BinaryCompatibleDefaultMethods
From APIDesign
Line 1: | Line 1: | ||
- | [[DefaultMethods]] are useful when one desperately needs to add a method into an existing '''interface''' (check why you should [[DefaultMethods#Can_you_disagree.3F|strive to avoid that]]). However sometimes there is a need to [[DefaultMethods|do so]], then it is handy that [[DefaultMethods]] are available since '''JDK8'''. | + | [[DefaultMethods]] are useful when one desperately needs to add a method into an existing '''interface''' (check why you should [[DefaultMethods#Can_you_disagree.3F|strive to avoid that]]). However sometimes there is a need to [[DefaultMethods|do so]], then it is handy that [[DefaultMethods]] are available since '''JDK8'''. However, use with care. It has recently been demonstrated that adding [[DefaultMethods]] can compromise [[BinaryCompatibility]]. |
- | + | === Binary Incompatibility in CharSequence === | |
+ | |||
+ | Recently [https://twitter.com/emilianbold/status/1308677540408709125?s=20 Emilian Bold asked me] to participate in a tweeting about [[BinaryCompatible|binary incompatibility]] caused by adding new {{JDK|java/lang|CharSequence}}'''.isEmpty''' default method in '''JDK15'''. An interesting case. To summarize: Following code compiles and runs on '''JDK8''' to '''JDK14''': | ||
<source lang="java"> | <source lang="java"> | ||
Line 53: | Line 55: | ||
</source> | </source> | ||
- | Why? Since '''JDK15''' there is the new '''CharSequence.isEmpty()''' [[DefaultMethods|default method]]. As such, when the [[JVM]] wants to processes the '''CharArrayLike''' class it doesn't know whether to dispatch to the '''ArrayLike.isEmpty()''' or to the newly added method. | + | Why it fails? Since '''JDK15''' there is the new '''CharSequence.isEmpty()''' [[DefaultMethods|default method]]. As such, when the [[JVM]] wants to processes the '''CharArrayLike''' class it doesn't know whether to dispatch to the '''ArrayLike.isEmpty()''' or to the newly added method. |
+ | |||
+ | === Dispatch Directly === | ||
+ | |||
+ | It is unfortunate that a code that used to run on older [[JDK]]s, doesn't even link and results in a linkage error on '''JDK15'''. The fix is however ''simple'' - just override the method and dispatch manually: | ||
<source lang="java"> | <source lang="java"> | ||
Line 64: | Line 70: | ||
</source> | </source> | ||
- | + | Such fix has to be made across all affected places. Then all applications using such libraries need to update to the latest version. Only then such applications can run on '''JDK15'''. That no longer sounds as ''simple'', right? Clearly [[DefaultMethods]] aren't a heaven sent solution, they have some cost! If you are writing an [[API]] and want to avoid your customers paying that cost, then [[APIvsSPI|separate API from SPI]] and don't mix the [[APIvsSPI|two concepts]] (especially in types that are frequently implemented by users of your [[API]]). | |
Revision as of 08:23, 29 September 2020
DefaultMethods are useful when one desperately needs to add a method into an existing interface (check why you should strive to avoid that). However sometimes there is a need to do so, then it is handy that DefaultMethods are available since JDK8. However, use with care. It has recently been demonstrated that adding DefaultMethods can compromise BinaryCompatibility.
Contents |
Binary Incompatibility in CharSequence
Recently Emilian Bold asked me to participate in a tweeting about binary incompatibility caused by adding new CharSequence.isEmpty default method in JDK15. An interesting case. To summarize: Following code compiles and runs on JDK8 to JDK14:
public interface ArrayLike { int length(); default boolean isEmpty() { return length() == 0; } } final class CharArrayLike implements CharSequence, ArrayLike { private final char[] chars; CharArrayLike(char... chars) { this.chars = chars; } @Override public int length() { return chars.length; } @Override public char charAt(int index) { return chars[index]; } @Override public CharSequence subSequence(int start, int end) { return new String(chars, start, end); } public static void main(String... args) { boolean empty = new CharArrayLike('E', 'r', 'r', 'o', 'r', '!').isEmpty(); System.err.println("not empty: " + empty); } }
However trying to run the code on JDK15, gives a linkage error:
$ /jdk-14/bin/javac ArrayLike.java $ /jdk-14/bin/java CharArrayLike not empty: false $ /jdk-15/bin/java CharArrayLike Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: java/lang/CharSequence.isEmpty ArrayLike.isEmpty at CharArrayLike.isEmpty(ArrayLike.java) at CharArrayLike.main(ArrayLike.java:32)
Why it fails? Since JDK15 there is the new CharSequence.isEmpty() default method. As such, when the JVM wants to processes the CharArrayLike class it doesn't know whether to dispatch to the ArrayLike.isEmpty() or to the newly added method.
Dispatch Directly
It is unfortunate that a code that used to run on older JDKs, doesn't even link and results in a linkage error on JDK15. The fix is however simple - just override the method and dispatch manually:
final class CharArrayLike implements CharSequence, ArrayLike { @Override public boolean isEmpty() { return ArrayLike.super.isEmpty(); } }
Such fix has to be made across all affected places. Then all applications using such libraries need to update to the latest version. Only then such applications can run on JDK15. That no longer sounds as simple, right? Clearly DefaultMethods aren't a heaven sent solution, they have some cost! If you are writing an API and want to avoid your customers paying that cost, then separate API from SPI and don't mix the two concepts (especially in types that are frequently implemented by users of your API).
Why now? Why not then?
It would be possible to end the story here, but let's go on. The CharSequence.isEmpty() case is not the first time a CharSequence interface was enhanced with additional DefaultMethods. In JDK8 the interface got two new methods:
public default IntStream chars(); public default IntStream codePoints();
No problem happened back then. How comes? No clash happened. Was it just a pure luck?
No! It wasn't a luck. The JDK8 addition of chars() and codePoints() was BinaryCompatible! Because adding new methods into an existing type cannot clash, if one of the arguments, or a return type is a newly introduced type. IntStream was also added in JDK8 - as such the methods couldn't clash with any code compiled on JDK7. A method invocation reference in a bytecode encodes not only name of the method, but also types of all parameters and return type. As such it was impossible on JDK7 to compile method with exactly the same signature.
Moral of the story? When trying to add DefaultMethods into existing interfaces in a 100% compatible way, also add a new type. Then it has to be BackwardCompatible! Adding simple signatures like boolean isEmpty() may clash. Adding complex signature like IntStream codePoints() cannot!
Apply the Knowledge
Now, when we know how to enhance interfaces with DefaultMethods compatibly, let's continue the mental experiment and apply the know-how to the CharSequence.isEmpty() case. Let's be super modern and let's use record feature:
interface CharSequence { /** @since 15 */ record Status(boolean empty); /** @since 15 */ default Status status() { return Status(length() == 0); } }
That'd be completely compatible evolution. People would have to write seq.status().empty() instead of simple seq.isEmpty(). But does that matter? Not from a performance perspective - any good JIT compiler eliminates any overhead. Then it is just about the will to be 100% compatible or the lack of it.
Moreover this kind of evolution "mounts" an open space of ClientAPI on the CharSequence interface. E.g. in future versions it will be easy to expand the Status class with new attributes. For example in JDK23 one could add:
interface CharSequence { record Status(boolean empty, boolean large, boolean asciiOnly); default Status status() { return Status(length() == 0, length() > 255, codePoints().allMatch((ch) -> ch <= 127); } }
That'd be a BinaryCompatible change (if one follows RecordEvolution rules properly). And so on, so on: New attributes could be added to the Status over time. Adding DefaultMethods to interfaces in a 100% BinaryCompatible way is possible at the end!