BinaryCompatibleDefaultMethods

From APIDesign

(Difference between revisions)
Jump to: navigation, search
Line 1: Line 1:
-
[[DefaultMethods]] are useful when one desperately needs to add a method into an existing '''interface'''. However, they decrease [[clarity]] of a [[ProviderAPI]] (no, you [[DefaultMethods#Can_you_disagree.3F|can't disagree!]]). As such, don't overuse. Morever it has been recently demonstrated that adding [[DefaultMethods]] can even compromise [[BinaryCompatibility]]. 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 {{JDK|java/lang|CharSequence}}'''.isEmpty''' in JDK15. An interesting case. Following code compiles on JDK8 to JDK14:
+
[[DefaultMethods]] are useful when one desperately needs to add a method into an existing '''interface'''. However, they decrease [[clarity]] of a [[ProviderAPI]] (no, you [[DefaultMethods#Can_you_disagree.3F|can't disagree!]]). As such, use with care. Rather invest into propery [[APIvsSPI|client API and provider API separation]] to begin with.
 +
 
 +
Morever it has recently been demonstrated that adding [[DefaultMethods]] can even compromise [[BinaryCompatibility]]. 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 {{JDK|java/lang|CharSequence}}'''.isEmpty''' in JDK15. An interesting case. Following code compiles and runs on JDK8 to JDK14:
<source lang="java">
<source lang="java">
Line 39: Line 41:
</source>
</source>
-
While the code compiles and runs fine with '''JDK14''' and older, it can be no longer compiled on '''JDK15'''. It results in following error:
+
However trying torun the code on JDK15, gives a linkage error.
<source lang="bash">
<source lang="bash">
Line 45: Line 47:
$ /jdk-14/bin/java CharArrayLike
$ /jdk-14/bin/java CharArrayLike
not empty: false
not empty: false
-
$ /jdk-15/bin/javac ArrayLike.java
 
-
ArrayLike.java:9: error: types CharSequence and ArrayLike are incompatible;
 
-
final class CharArrayLike implements CharSequence, ArrayLike {
 
-
^
 
-
class CharArrayLike inherits unrelated defaults for isEmpty() from types CharSequence and ArrayLike
 
-
1 error
 
-
</source>
 
-
 
-
Why? Since '''JDK15''' there is '''CharSequence.isEmpty()''' [[DefaultMethods|default method]]. As such, when '''javac''' processes the '''CharArrayLike''' class it doesn't know whether to select the '''ArrayLike.isEmpty()''' for delegation or the newly added method. [[SourceCompatibility]] is compromised! However that is not that big deal. Keeping [[SourceCompatibility]] in [[Java]] is hard. [[SourceCompatibility]] can be compromised by almost any addition. However the problem is that the above change also compromises [[BinaryCompatibility]] (I'd say the '''javac''' error is more a warning of the future runtime failure than real problem for '''javac'''). Compile against '''JDK14''' [[API]]s, you'll get runtime error on '''JDK15''':
 
-
 
-
<source lang="bash">
 
-
$ /jdk-15/bin/javac --release 14 ArrayLike.java
 
$ /jdk-15/bin/java CharArrayLike
$ /jdk-15/bin/java CharArrayLike
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: java/lang/CharSequence.isEmpty ArrayLike.isEmpty
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: java/lang/CharSequence.isEmpty ArrayLike.isEmpty
Line 63: Line 53:
</source>
</source>
-
Well, that's bad. A code that used to work on previous version of [[Java]] no longer works on new one. Why? Because adding methods (even [[DefaultMethods]]) to classes that are be implemented by 3rd parties is never 100% [[BinaryCompatible]]. The fix is simple. Just add:
+
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 link to the '''ArrayLike.isEmpty()''' or the newly added method. That means a code that used to run on older [[JDK]]s, don't even link and results into a runtime error on '''JDK15'''. That's not [[good]]. A code that used to work on previous version of [[Java]] no longer works on new [[JDK]]. The fix is ''simple''. Just add:
<source lang="java">
<source lang="java">
Line 74: Line 64:
</source>
</source>
-
but such fix has to be made across all libraries. All applications using such libraries need to update to their latest versions and only then they can run on '''JDK15'''. 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]]).
+
but such fix has to be made across all affected libraries. All applications using such libraries need to update to the latest libraries versions and only then they 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 15:36, 28 September 2020

DefaultMethods are useful when one desperately needs to add a method into an existing interface. However, they decrease clarity of a ProviderAPI (no, you can't disagree!). As such, use with care. Rather invest into propery client API and provider API separation to begin with.

Morever it has recently been demonstrated that adding DefaultMethods can even compromise BinaryCompatibility. Recently Emilian Bold asked me to participate in a tweeting about binary incompatibility caused by adding CharSequence.isEmpty in JDK15. An interesting case. 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 torun 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? 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 link to the ArrayLike.isEmpty() or the newly added method. That means a code that used to run on older JDKs, don't even link and results into a runtime error on JDK15. That's not good. A code that used to work on previous version of Java no longer works on new JDK. The fix is simple. Just add:

final class CharArrayLike implements CharSequence, ArrayLike {
    @Override
    public boolean isEmpty() {
        return ArrayLike.super.isEmpty();
    }
}

but such fix has to be made across all affected libraries. All applications using such libraries need to update to the latest libraries versions and only then they 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? 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 a new DefaultMethods. In JDK8 the interface got two new methods:

public default IntStream chars();
public default IntStream codePoints();

No problem happened back then. How comes? Was the JDK team just lucky that no clash happened?

No, the JDK8' addition of chars() and codePoints() was BinaryCompatible! Because adding new methods into existing types cannot clash, if one of the arguments, or a return type is new. As IntStream was also added in JDK8 the methods couldn't clash with any code compiled on JDK7. A method 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 the same signature. Quite different to JDK15 adding ```boolean isEmpty()``` identified by boolean a core type available since JDK1.0.

Moral of the story? When trying to add DefaultMethods into existing interfaces in a 100% compatible way, also add a new type. That it has to be BackwardCompatible!

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:

interface CharSequence {
  record Status(boolean empty);
 
  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 the overhead. Then it is just about the will to be 100% compatible and the lack of it.

Moreover this kind of evaluation "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 compatible 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!

Personal tools
buy