Java 9 is near to come and more features will be added to Java interfaces, like private methods. default
methods in interfaces were added in Java 8, essentially to support the use of lambdas inside collections without breaking retro-compatibility with previous versions of the language.
In Scala, methods inside trait
s are quite useful. However, Scala has a different approach treating trait
s than Java with default
methods. Think to multiple inheritance resolution or the use of trait
s as mixins.
Apart the above use, which are the real scenarios in which using a default
methods is worth? During this years is it arisen some pattern that uses them? Which problems can I solve using this kind of methods?
Default methods enable you to add new functionality to existing interfaces and ensure binary compatibility with code written for older versions of those interfaces. In particular, default methods enable you to add methods that accept lambda expressions as parameters to existing interfaces.
All the methods in an interface must be abstract, you cannot have a concrete method (the one which has body) if you try to do so, it gives you a compile time error saying “interface abstract methods cannot have body”.
Interfaces can have default methods with implementation in Java 8 on later. Interfaces can have static methods as well, similar to static methods in classes. Default methods were introduced to provide backward compatibility for old interfaces so that they can have new methods without affecting existing code.
Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces. A static method is a method that is associated with the class in which it is defined rather than with any object.
Brian Goetz and I covered some of this at our JavaOne 2015 talk, API Design with Java 8 Lambda and Streams. Despite the title, there is some material at the end about default methods.
Slides: https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf
Video: https://youtu.be/o10ETyiNIsM?t=24m
I'll summarize here what we said about default methods.
Interface Evolution
The primary use case of default methods is interface evolution. Mainly, this is the ability to add methods to interfaces without breaking backward compatibility. As noted in the question, this was most prominently employed to add methods allowing conversion of Collections to Streams and to add lambda-based APIs to Collections.
There are several other use cases, though.
Optional Methods
Sometimes interface methods are logically "optional". Consider mutator methods on immutable collections, for example. Of course, an implementation is required, but usually what it will do in such cases is to throw an exception. This can easily be done in a default method. Implementations can inherit the exception-throwing method if they don't want to provide it, or they can override it if they want to provide an implementation. Example: Iterator.remove
.
Convenience Methods
Sometimes a method is provided for the convenience of callers, and there is an obvious and optimal implementation. This implementation can be provided by a default method. It's legal for an implementation to override the default, but there's generally no reason, so implementations will usually inherit it. Examples: Comparator.reversed
, Spliterator.getExactSizeIfKnown
, Spliterator.hasCharacteristics
. Note that Spliterator
was introduced in Java 8, including the default methods, so this clearly wasn't a case of interface evolution.
Simple Implementation, Intended to be Overridden
A default method can provide a simple, general implementation that works for all implementations, but that is probably suboptimal. This assists implementations during initial bring-up, because they can inherit the default and be assured of correct operation. However, in the long term, implementations will probably want to override the default and provide an improved, customized implementation.
Example: List.sort
. The default implementation copies the list elements to a temporary array, sorts the array, and copies the elements back to the list. This is a correct implementation, and sometimes it can't be improved upon (e.g. for LinkedList
). However, ArrayList
overrides sort
and sorts its internal array in-place. This avoids the copying overhead.
Now, obviously sort
was retrofitted onto List
and ArrayList
in Java 8, so the evolution didn't happen this way. But you could easily imagine bringing up a new List
implementation. You'd probably initially inherit the sort
default implementation while you're getting the basics implemented properly. Later on, you might consider implementing a customized sort algorithm that's tuned to your new implementation's internal data organization.
First that comes to mind is the use of default methods to support some functional programming techniques:
@FunctionalInterface
public interface Function3<A, B, C, D> {
D apply(A a, B b, C c);
default Function<A, Function<B, Function<C, D>>> curry() {
return a -> b -> c -> this.apply(a, b, c);
}
default Function<B, Function<C, D>> bindFirst(A a) {
return b -> c -> this.apply(a, b, c);
}
}
Sample usage:
Function3<Long, Long, Long, Long> sum = (a, b, c) -> a + b + c;
long result = sum.apply(1L, 2L, 3L); // 6
Function<Long, Function<Long, Function<Long, Long>>> curriedSum = sum.curry();
result = curriedSum.apply(1L).apply(2L).apply(3L); // 6
Function<Long, Function<Long, Long>> incr = sum.bindFirst(1L);
result = incr.apply(7L).apply(3L); // 11
result = incr.apply(6L).apply(7L); // 14
You can have similar binding methods for the other parameters, implemented with default methods, such as bindSecond
and bindThird
.
You can use default methods to decorate the parent interface (as @holi-java explains in his answer), also there a lot of examples of the adapter pattern (currying and binding are actually adapters).
Besides functional programming, you can use default methods to support kind of, limited multiple inheritance:
public interface Animal {
String getHabitat();
}
public interface AquaticAnimal extends Animal {
@Override
default String getHabitat() {
return "water";
}
}
public interface LandAnimal extends Animal {
@Override
default String getHabitat() {
return "ground";
}
}
public class Frog implements AquaticAnimal, LandAnimal {
private int ageInDays;
public Frog(int ageInDays) {
this.ageInDays = ageInDays;
}
public void liveOneDay() {
this.ageInDays++;
}
@Override
public String getHabitat() {
if (this.ageInDays < 30) { // is it a tadpole?
return AquaticAnimal.super.getHabitat();
} // else
return LandAnimal.super.getHabitat();
}
}
Sample:
Frog frog = new Frog(29);
String habitatWhenYoung = frog.getHabitat(); // water
frog.liveOneDay();
String habitatWhenOld = frog.getHabitat(); // ground
Maybe not the best example, but you get the idea...
Another usage would be traits:
public interface WithLog {
default Logger logger() {
return LoggerFactory.getLogger(this.getClass());
}
}
public interface WithMetrics {
default MetricsService metrics() {
return MetricsServiceFactory.getMetricsService(
Configuration.getMetricsIP(
Environment.getActiveEnv())); // DEV or PROD
}
}
Now, whenever you have a class that needs to log something and report some metrics, you could use:
public class YourClass implements WithLog, WithMetrics {
public void someLongMethod() {
this.logger().info("Starting long method execution...");
long start = System.nanoTime();
// do some very long action
long end = System.nanoTime();
this.logger().info("Finished long method execution");
this.metrics().reportExecutionTime("Long method: ", end - start);
}
}
Again, this is not the best possible implementation, but just sample code to see how traits can be used via default methods.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With