Imagine a bacon-wrapped Ferrari. Still not better than our free technical reports.
See all our reports

How your addiction to Java 8 default methods may make pandas sad and your teammates angry!

Panda is sad and doesn't want to look at us

It’s been some time now since Java 8 was released into general availability. We’ve now gathered some experience with it, got a chance to apply some cool new features to projects of different complexities. Some people are brave enough to use it now, as they told us themselves in their Java Tools and Technology Landscape for 2014, others are more careful. But sooner or later Java 8 will slip in production environments.

There were 3 major features in the Java 8 release that got everyone talking: Lambdas, the Streams API and Default Methods in interfaces. There are others as well, and one might call Streams just an API, but undoubtedly, these are the big trinity to pay attention to.

Today I want to talk about the default methods in interfaces. Note, that this post is not about the pitfalls of a certain aspect of the Java language, instead it has a more philosophical takeaway: use new features with caution, don’t rewrite your system using only interfaces just because you can, and think about those who will have to read the code after you. In general, lots of sad pandas are encountered along the way of changing your codebase for the sake of coolness and novelty. Trust me!

Anyway the default methods in interfaces were introduced to the Java language in order to evolve the collections library for the Streams API. Default methods allow an interface in addition to declaring methods actually supply their implementations. So all the subclasses implementing the interface but lacking the implementation of the freshly added methods won’t break right then and there. If you want to refresh your memory, check this detailed blogpost on default methods in Java 8 by the ZeroTurnaround product lead for JRebel, Anton Arhipov.

The rules of resolving a method implementation are quite straightforward:

  • A concrete implementation in the class wins
  • The lowest implementation in the implemented interfaces wins
  • If there are multiple implementations available through different interfaces that are on different paths up through the class hierarchy, the program doesn’t compile.

Now, I DO in fact think this is a great contribution to the Java language, but it is also rather interesting to consider how developers can easily, if not unwittingly, abuse default methods if given the chance.

One of the things that come to mind when we talk about default methods in interfaces is Traits. A trait is a piece of functionality that can be declaratively added to a type. Sounds awesome doesn’t it? I have a container with the random access, I add Sortable trait aka the following interface:

public interface Sortable<T> {

   <T> T get(int idx); 
   <T> void set(T t, int idx); 
   
   void sort() default {
      // here is an implementation of sort using get() and set()
      // maybe I need a size() as well, but that’s not the point
      return; 
   }
}

Amazing, now every container I mark with Sortable now supports sorting. But beware: things can easily go wrong when I have a complex type system and several libraries present.

For example, imagine that you have four types, two classes and two interfaces:

package org.shelajev.throwaway.defmeth;

public class Main {
  public static void main(String[] args) {
    // what will this program produce?
    B b = new B();
    b.m();
    b.callM();
    b.callSuperM();

    System.out.println("============");

    A a = new B();
    a.m();
    a.callM();
  }
}

class A implements I1 {
  public void callM() {
    m();
  }
}

class B extends A implements I2 {
  public void callSuperM() {
    super.m();
  }
}

interface I1 {
  default void m() {
    System.out.println("I1");
  }
}

interface I2 extends I1 {
  default void m() {
    System.out.println("I2");
  }
}

Now the caveat here is that when we have an instance of A, we don’t really know which implementation of m() will be called from the callM() call.

Naturally, we would expect that to come from an interface, because no class provides a concrete implementation themselves; however, our class B here implements a sneaky interface I2, which overrides / overloads (?) the implementation and makes things fuzzy.

The program above actually outputs the following:

I2
I2
I1
============
I2
I2

This is easy to see in this synthetic tiny example, but in a real world system, reasoning about the program flow will be much harder.

The worst thing is that your IDE cannot help you here too. Until runtime kicks in, you cannot be sure what implementation sits behind the generic interface type variable. So, in addition to 16 layers of interfaces calling each other that we so often encounter in the enterprise applications, we will have to crawl through a jungle of default methods in these interfaces.

Now, I understand that this kind of code is not so common in your average application, however, if you encourage the use of default methods as traits, you will get those problems.

And if you’re not convinced, check out this older thread from the openjdk mail list, where Brian Goetz explains some reasons why default methods were designed the way they were.

To conclude, I would like you to forget about the default methods in interfaces until the moment you find yourself with an widely popular, but somewhat outdated library that you want to evolve. Then the default methods are the right tool for the job. Until then, go with a single inheritance and make people reading your code happier!

If you have an opinion, don’t hesitate to leave a comment or find me on Twitter: @shelajev.

If you’re interested in the Java 8 vs Java 7 performance benchmark, check out that post as your next read! Alternatively also look at how you can use Java 8 Streams, filters, maps and foreach in another amazing RebelLabs post.


Default methods are really cool, but should be used sparingly. Find out more about the new features in Java 8 with “Java 8 Revealed: Lambdas, Default Methods and Bulk Data Operations”.

DOWNLOAD THE FULL REPORT

  • I think it is worth noting that Oleg is the first one to coin the awesome term sneaky interface :-)

    On a more serious note, the main takeaway from your post is the same one as always in these situations. Once you entangle your type hierarchy in such a way, you’re in for trouble. The best solution is to keep type hierarchies simple and straight-forward and to favour composition over inheritance – and this now becomes more true than ever, now that we can have implementations in interfaces.

  • Oleg Šelajev

    Thanks Lukas, this is exactly what I want readers to understand and remember!

  • Kshitiz Garg

    Thanks for such a useful post. In addition to the above ‘sneaky’ beahavior, I believe that default methods ruin interfaces. Interfaces are meant to decouple implementations. But now with default methods, client objects get default methods in their face even if they might not like to have them. What is your view ?

  • Dimitry Sherman

    Maybe I am missing something, but you are not creating an instance of A (it’s still an instance of B), hence the behavior is predictable. Please, correct me if I am wrong.
    Thank you.

  • Marius Filip

    Oleg, Brian Goetz does not handle your situation but a completely different abuse: using default methods to emulate state – which is stupid, of course, but when you break the contract (interfaces are stateless, don’t make them statefull) you can’t expect your program to behave.

    Your situation illustrates another type of breaching of interface contract: an overriding method MUST satisfy the same semantics than the overridden method. Once you have that, what method gets called doesn’t really matter. But there is nothing new under the sun here, this principle existed from day one in object orientation, and if you break it, then you expect unpredictable behaviour. Nota bene, this applies to classes as much as it applies to interfaces with default methods.

    I claim that, when you have interfaces with default methods and you keep both principles satisfied:
    1) keep interfaces truly stateless (it applies for any interface, with default methods or not)
    2) an overriding method must always be semantically equivalent to the overridden method (it applies for any class, not just interfaces)

    … then interfaces with default methods behave exactly like traits – whose main purpose is composition of behaviour – and I argue that, in fact, they are better than traits.

    Moreover, default methods have another advantage – and I’m surprised that no-one captured this: they offer a behavioural blue-print, something that you can test against.

    For example, you can have an interface A with default method m() that lays our what m() means. But you can optimize A.m() into class AImpl.m() – for example, by caching the outcome of A.m() if that makes sense.

    Now, you can have a test suite that, given any descendant X of A, X.m() will always behave like A.m() – which is a principle so fundamental to object-orientation inheritance that is so often overlooked, most often with very bad consequences.

  • seahen

    I don’t think the diamond problem will actually happen very often without an underlying mistake in the ontology (e.g. dual inheritance doing the job of composition). I’ll still use one anywhere it eliminates duplicate code that can’t all be eliminated by either of the alternatives (making either parent the class and leaving the other as a “pure” interface).