Java 8 default methods can break your (users') code

Java 8 default methods are a promising attempt to make the evolution of Java APIs easier. Unfortunately, this recent language extension also brought along a complex set of rules only few Java developers are yet aware of. This article explains why the introduction of default methods can break your (users’) code.

At first glance, default methods brought a great new feature to the Java Virtual Machine's instruction set. Finally, library developers are able to evolve established APIs without introducing incompatibilities to their user's code. Using default methods, any user class that implements a library interface automatically adopts the default code when a new method is introduced to this interface. And once a user updates his implementing classes, he can simply override the default with something more meaningful to his particular use case. Even better, the user can call the default implementation of the interface from the overridden method and add logic around it.

So far, so good. However, adding default methods to established interfaces can render Java code uncompilable. This is easiest to understand when looking at an example. Let us assume a library that requires a class of one of its interfaces as its input:

interface SimpleInput {
  void foo();
  void bar();
}

abstract class SimpleInputAdapter implements SimpleInput {
  @Override
  public void bar() {
    // some default behavior ...
  }
}

Prior to Java 8, the above combination of an interface and a corresponding adapter class is a rather common pattern in the Java programming language. The adapter is usually offered by the library supplier to save the library's users some typing. However, the interface is offered additionally in order to allow an approximation of multiple inheritance.

Let us further assume that a user made use of this adapter:

class MyInput extends SimpleInputAdapter {
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    super.bar();
    // do something additionally ...
  }
}

With this implementation, the user can finally interact with the library. Note how the implementation overrides the bar method to add some functionality to the default implementation.

So what happens if the library migrates to Java 8? First of all, the library will most likely deprecate the adapter class and move the functionality to default methods. As a result, the interface will now look like this:

interface SimpleInput {
  void foo();
  default void bar() {
    // some default behavior
  }
}

With this new interface, a user can update his code to adapt the default method instead of using the adapter class. The great thing about using interfaces instead of adapter classes is the ability to extend another class than the particular adapter. Let's put this into action and migrate the MyInput class to use the default method instead. Because we can now extend another class, let us additionally extend some third party base class. What this base class does is not of particular relevance here, so let us just assume that this makes sense for our use-case.

class MyInput extends ThirdPartyBaseClass implements SimpleInput {
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    SimpleInput.super.bar();
    // do something additionally ... 
  }
}

To implement similar behavior as in the original class, we make use of Java 8's new syntax for calling a default method of a specific interface. Also, we moved the logic for myMethod to some base class MyBase. Clap on our shoulders. Nice refactoring here!

The library we are using is a great success. However, the maintainer needs to add another interface to offer more functionality. This interface represents a ComplexInput which extends the SimpleInput with an additional method. Because default methods are in general considered as being safe to add, the maintainer additionally overrides the SimpleInput's default method and adds some behavior to offer a better default. After all, doing so was quite common when implementing adapter classes:

interface ComplexInput extends SimpleInput {
  void qux();
  @Override
  default void bar() {
    SimpleInput.super.bar(); 
    // so complex, we need to do more ...
  }
} 

This new feature turns out so great that the maintainer of the ThirdPartyBaseClass decided to also rely on this library. For making this work, he implements the ComplexInput interface for the ThirdPartyLibrary.

But what does that mean to the MyInput class? Due to the implicit implementation of ComplexInput by extending ThirdPartyBaseClass, calling the default method of SimpleInput has suddenly become illegal. As a result, the user's code does not longer compile. Also, it is now generally forbidden to call this method since Java sees this call as illegal as calling a super-super method of an indirect super class. Instead, you could call the default method of the ComplexInput class. However, this would require you to first explicitly implement this interface in MyInput. For the user of the library, this change comes most likely rather unexpected!

Oddly enough, the Java runtime does not make this distinction. The JVM's verifier will allow a compiled class to call SimpleInput::foo even if the loaded class at run time implicitly implements the ComplexClass by extending the updated version of ThirdPartyBaseClass. It is only the compiler that complains here.

But what do we learn from this? In a nutshell, make sure to never override a default method in another interface. Neither with another default method, nor with an abstract method. In general, be careful about using default methods at all. As much as they ease the evolution of established APIs as Java's collection interfaces, they are intrinsically complex by allowing to perform method invocations that go sideways in your type hierarchy. Before Java 7, you would only need to look for the actually invoked code by traversing down a linear class hierarchy. Only add this complexity when you really feel it is absolutely necessary.

 

The article can also be found on Rafael's own blog or on DZone.

 

Litt om forfatteren

Rafael Winterhalter

Rafael Winterhalter

Rafael Winterhalter er systemutvikler i Kantega i Oslo. Han er en Java-entusiast med spesiell interesse for byte code engineering, funksjonell programmering, multi-threading og programmeringsspråket Scala.
comments powered by Disqus