Blog-Archiv

Sonntag, 28. März 2021

Java Interface Default Implementations

Since Java 8 we can write implementations into interfaces. As several interfaces can be implemented by a class, a class can now inherit actual code (not just responsibilities) from multiple different sources. Such inherited default implementations are overridable, and multiple-inheritance problems will detected and rejected by the Java 8 compiler.

Example

The interface Identified demands the presence of an identifier like a database primary key.
The interface Named demands the presence of a human readable name.
Objects of the example class Person should be both identified and named.

    public static void main(String[] args) {
        Person person = new Person(1L, "John Doe");
        System.out.println("Person id="+person.getId()+", name="+person.getName());
    }

This should output:

Person id=1, name=John Doe

Before Java 8

It was not possible to provide a default implementation of an interface responsibility, all implementations had to be in the class:

public interface Identified
{
    Object getId();
}

public interface Named
{
    String getName();
}

public class Person implements Identified, Named
{
    private final Object identity;
    private final String name;
   
    public Person(Object identity, String name)   {
        this.identity = identity;
        this.name = name;
    }
   
    @Override
    public Object getId() {
       return identity;
    }

    @Override
    public String getName() {
       return name;
    }
}

Since Java 8

In case the inherited interface responsibility happens to be optional, we can now provide a default implementation for any interface method:

public interface Named
{
    default String getName() {
        return "(undefined name)";
    }
}

public interface Identified
{
    default Object getId() {
        return null;
    }
}

As both interfaces provide defaults, the inheriting class is not required to implement anything:

public class Person implements Identified, Named
{
}

This compiles!
Output will be:

Person id=null, name=(undefined name)

Overriding Defaults

Most important here: default implementations can be overridden (other than static ones). In the Person example this actually makes sense. Following source shows an implementation that redirects getId() to the according constructor parameter:

public class Person implements Identified, Named
{
    private final Object identity;
   
    public Person(Object identity)   {
        this.identity = identity;
    }
   
    @Override
    public Object getId() {
       return identity;
    }
}

Conflict Solution

The "diamond problem" occurs in case two inherited interfaces provide a default method of same signature:

public interface Identified
{
    default boolean isDeleted() {
        return false;
    }
    
    ....
}

public interface Named
{
    default boolean isDeleted() {
        return true;
    
    ....
}

The isDeleted() method has been introduced on both interfaces, with different default implementations. Which one would be used at runtime by Person?

We don't need to think about this. Following error message is printed by the Java compiler when it comes to class Person:

Duplicate default methods named isDeleted with the parameters () and () are inherited from the types Named and Identified.

Following override would fix it:

public class Person implements Identified, Named
{
    ....

    @Override
    public boolean isDeleted() {
        return Identified.super.isDeleted();
    }

}

The class decides for one of the defaults, in this case for the implementation in interface Identified.

Conclusion

Interface default implementations could easily take over the role that static utility classes have been playing until now (StringUtils, IOUtils, ...). Theses classes provided "pure" method implementations that rely on parameters only, without using their enclosing class body in any way. The problem with these utils is that they are not overridable. Interface default methods are overridable and would solve this problem. Method signature clashes would be found by the compiler.

Keep in mind that interfaces still can not hold any state, that means default implementations need to be "pure", everything they need has to be passed to them as parameter. But an interface now is the perfect place for "pure" functions.

Mind further that interfaces methods are always public, implicitly. You can not reduce their visibility to protected or private in an override.




Keine Kommentare: