Performance Zone is brought to you in partnership with:

Rafael is a software engineer at Kantega based in Oslo. He is a Java enthusiast with particular interests in byte code engineering, functional programming, multi-threaded applications and the Scala language. Rafael is a DZone MVB and is not an employee of DZone and has posted 5 posts at DZone. You can read more from them at their website. View Full User Profile

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

05.16.2014
| 16066 views |
  • submit to reddit

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.

 

 

Published at DZone with permission of Rafael Winterhalter, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Holger Stenger replied on Wed, 2014/05/21 - 6:22pm

You have nicely described the dangers of using default methods. I also have very mixed feelings about them.

I noticed something curious about your example. The reason for introducing default methods to the Java language was to allow evolving interfaces without breaking (binary) compatibility with existing implementations. However you would do just that, if you moved a default implementation from an adapter class to default of the interface and later on removed the adapter class. This would only postpone the point were you break compability. I would rather keep a meaningful adapter class than drag a deprecated adapter class along for an indefinite amount of time.

Gabriel Marcelli replied on Thu, 2014/05/22 - 1:57am

 I honestly think that default methods do more harm than good.

With Java 8, the language effectively shifted from Single inheritance to Multiple inheritance, and did it without introducing a clear-cut separation between the Old and the New ways.

I agree the language now has greater potential than did before but what was the cost? Greater complexity, greater maintenance costs for existing software, need for much more skilled developers.

Mohammed Nazim ... replied on Thu, 2014/05/22 - 2:05am

A good read on danger of taking default methods for granted.

The example you've given shows how important it is to have a good OO entity design. If MyInput extends ThirdPartyBaseClass (which is already a SimpleInput) then there is no need for MyInput to implement SimpleInput. Before java8 this was not a big issue, but now with default methods for interfaces this practice should be flagged as a potential bug. Might be good to have a rule for this in static code analyzers.

Piotr Henrykowicz replied on Mon, 2014/05/26 - 4:51pm

 I don't really see the issue in such bad colours.

If I remember correctly, the pure interfaces (without any implementation) in previous versions of Java were made so to avoid the multi-inheritance problem.

IMHO the MIP is exaggerated because of the C++, where you couldn't bee sure which implementation will be used in the runtime. However, if you can be 100% sure which parent will deliver the implementation, there's no problem. This is e.g. the way Scala works with Traits - you can implement the method in a number of Traits and them mix them all in, but you know, which implementation will win at the end.

Two things though...
"Just because you can, doesn't mean you should." So I also hope, that the default methods won't be overused.
Second thing: you should be 100% sure, that your code and the libraries it uses work every time, no matter in which version the library is. If you don't have your code (and you bottom) covered with good tests, your brain needs to worry.
Good tests = no worries about libraries changing contract.

Rafael Winterhalter replied on Tue, 2014/05/27 - 10:15am in response to: Piotr Henrykowicz

Hi Piotr,

you are right, this borderline case is not the end of the world and most developers will live without ever encountering the issue. However, it does concern library developers that offer interfaces to interact with their application. And therewith, it concerns somewhat the users of these libraries. The problem that could occur was a library developer "upgrading" an interface to a more specific subtype which is a rather normal evolution phase in today's Java landscape. If this upgrade is adopted by another class that also serves as a base-class for a third class, this could break the inheriting class.

Yes, this does not happen every day, but it happens. And I do not think many people are (yet) aware of the rather complex rules that apply for calling a default method and in my opinion, the strictness of when a default method is invokable is exaggerated. Also, I do not like the concept of some classes being uncompilable but still legal at run time. As of today, you could simply compile a class against an outdated class path to overcome this limitation. This bothers me as a concept.

Again, this is still not a big issue. However, one reason that Java is so popular is its clear language rules. Default methods and generic types are two major deviations from this rule. For generics, I still agree with their necessity. With default methods, I agree to a lesser extend. 

PS: Even with Java, you can not be sure which run time implementation of a method is called due to its (often) dynamic dispatch. It always depends on the class path version. Default methods are to a given extend dispatched dynamically, if a method is declared in a super interface of the default method target. This is however not different to a normal method invocation and actually the way Java was invented.

Peter Verhas replied on Sat, 2014/06/14 - 3:50pm

http://java.dzone.com/articles/java-8-default-methods-what

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.