The implementation is simple: take an interface, add a concrete method and attach the keyword default as a modifier. The result is that suddenly all existing implementations of your interface can use this code. In this first, simple example, I’ve added default method that returns the version number of an interface1.
public interface Version {
/**
* Normal method - any old interface method:
*
* @return Return the implementing class's version
*/
public String version();
/**
* Default method example.
*
* @return Return the version of this interface
*/
default String interfaceVersion() {
return "1.0";
}
}
You can then call this method on any implementing class.
public class VersionImpl implements Version {
@Override
public String version() {
return "My Version Impl";
}
}
You may ask: why is this cool? If you take the
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
The forEach method takes an instance of a class that implements the Consumer<T> interface as an argument. The Consumer<T> can be found in the new java.util.function package and is what Java 8 calls a functional interface, which is an interface containing only one method. In this case it's the method accept(T t) that takes one argument and has a void return.
The java.util.function package is probably one of the most important packages in Java 8. It contains a whole bunch of single method, or functional, interfaces that describe common function types. For example, Consumer<T> contains a function that takes one argument and has a void return, whilst Predicate<T> is an interface with a function that takes one argument and returns a boolean, which is generally used to write filtering lambdas.
The implementation of this interface should contain whatever it is that you previously wrote between your for loops brackets.
So what, you may think, what does that give me? If this wasn't Java 8 then the answer is "not much". To use the forEach(...) method pre Java 8 you'd need to write something like this:
List<String> list = Arrays.asList(new String[] { "A", "FirsT", "DefaulT", "LisT" });
System.out.println("Java 6 version - anonymous class");
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String t) {
System.out.println(t);
}
};
list.forEach(consumer);
But, if you combine this with lambda expressions or method references you get the ability to write some really cool looking code. Using a method reference, the previous example becomes:
list.forEach(System.out::println);
You can do the same thing with a lambda expression:
list.forEach((t) -> System.out.println(t));
All this seems to be in keeping with one of the big ideas behind Java 8: let the JDK do the work for you. To paraphrase statesman and serial philanderer John F Kennedy “ask not what you can do with your JDK ask what your JDK can do for you”2
Design Problems of Default Methods
That’s the new cool way of writing the ubiquitous for loop, but are there are problems with adding default methods to interfaces and if so, what are they and how did the guys on the Java 8 project fix them?
The first one to consider is inheritance. What happens when you have an interface which extends another interface and both have a default method with the same signature? For example, what happens if you have SuperInterface extended by MiddleInterface and MiddleInterface extended by SubInterface?
public interface SuperInterface {
default void printName() {
System.out.println("SUPERINTERFACE");
}
}
public interface MiddleInterface extends SuperInterface {
@Override
default void printName() {
System.out.println("MIDDLEINTERFACE");
}
}
public interface SubInterface extends MiddleInterface {
@Override
default void printName() {
System.out.println("SUBINTERFACE");
}
}
public class Implementation implements SubInterface {
public void anyOldMethod() {
// Do something here
}
public static void main(String[] args) {
SubInterface sub = new Implementation();
sub.printName();
MiddleInterface middle = new Implementation();
middle.printName();
SuperInterface sup = new Implementation();
sup.printName();
}
}
No matter which way you cut it, printName() will always print “SUBINTERFACE”.
The same question arises when you have a class and an interface containing the same method signature: which method is run? The answer is the 'class wins' rule. Interface default methods will always be ignored in favour of class methods.
public interface AnyInterface {
default String someMethod() {
return "This is the interface";
}
}
public class AnyClass implements AnyInterface {
@Override
public String someMethod() {
return "This is the class - WINNING";
}
}
Running the code above will always print out: "This is the class - WINNING"
Finally, what happens if a class implements two interfaces and both contain methods with the same signature? This is the age-old C++ diamond problem; how do you solve the ambiguity? Which method is run?
public interface SuperInterface {
default void printName() {
System.out.println("SUPERINTERFACE");
}
}
public interface AnotherSuperInterface {
default void printName() {
System.out.println("ANOTHERSUPERINTERFACE");
}
}
In Java 8’s case the answer is neither. If you try to implement both interfaces, then you’ll get the following error:
Duplicate default methods named printName with the parameters () and () are inherited from the types AnotherSuperInterface and SuperInterface.
In the case where you absolutely MUST implement both interfaces, then the solution is to invoke the ‘class wins’ rule and override the ambiguous method in your implementation.
public class Diamond implements SuperInterface, AnotherSuperInterface {
/** Added to resolve ambiguity */
@Override
public void printName() {
System.out.println("CLASS WINS");
}
public static void main(String[] args) {
Diamond instance = new Diamond();
instance.printName();
}
}
When to Use Default Methods
From a purist point of view the addition of default methods means that Java interfaces are no longer interfaces. Interfaces were designed as a specification or contract for proposed/intended behaviour: a contract that the implementing class MUST fulfil. Adding default methods means that there is virtually no difference between interfaces and abstract base classes3. This means that they're open to abuse as some inexperienced developers may think it cool to rip out base classes from their codebase and replace them with default method based interfaces - just because they can, whilst others may simply confuse abstract classes with interfaces implementing default methods. I'd currently suggest using default methods solely for their intended use case: evolving legacy interfaces without breaking existing code. Though I may change my mind.
1It’s not very useful, but it demonstrates a point...
2 John F Kennedy inauguration speech January 20th 1961.
3 Abstract base classes can have a constructor whilst interfaces can’t. Classes can have private instance variables (i.e. state); interfaces can’t
1 comment:
I liked your summary - use default methods as they were intended : don't abuse them. I could see how they could be overused.
Post a comment