Compared to the splashy, paradigm-shifting introduction of Lambda expressions and streams, default interface methods entered the picture with decidedly less fanfare when Java 8 launched last spring. Indeed, the language feature barely registers in Google trends (in fact I had to artificially expand the time frame of the search to get it to appear on the chart at all). Furthermore, it is simply not a language feature that developers actively use often - in 12 months of extensive Java 8 development and dozens of interfaces authored in that time, I have created a default interface method exactly 0 times.
So, how does this feature - with seemingly very little community interest and which most developers will never (knowingly) use - warrant the treatment of my first-ever SourceClear technical blog entry?
Default Interface Methods Defined
The Java Tutorials Documentation defines default interface methods:
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.
and then proceeds to supply a convoluted, 50+ line example. Let's take a look at a more basic (albeit majorly contrived) illustration.
Imagine we have an interface for a generic ItemProvider
- this might be a Data Source or Connection Pool (a simple implementation is shown to flesh out the example).
Example: Item Provider Interface:
public interface ItemProvider {
Collection<Item> getItems();
}
public class ListItemProvider implements ItemProvider {
private List<Item> items = populateItemList(); // not shown
public Collection<Item> getItems() {
return items;
}
}
Now, we want to extend this interface with some additional functionality; say by adding a convenience method to fetch the first item (or null, if the collection is empty):
public interface ItemProvider {
Collection<Item> getItems();
Item getFirstItem();
}
public class ListItemProvider implements ItemProvider {
// ... previous implementation omitted
public Item getFirstItem() {
if (items == null || items.isEmpty()) {
return null;
}
return items.get(0);
}
}
Voila - a win for lazy developers everywhere... except, suddenly we remember that in addition to our simple ListItemProvider
, we also have a SetItemProvider
, a MapValueItemProvider
, a RandomOrderedItemProvider
, an AlwaysReturnEmptyItemProvider
... and (for some inexplicable reason) an AlwaysReturnNullItemProvider
. Hmmmm, we could fix this with some refactoring to extract code into abstract base classes. But suddenly, adding this simple functionality has lead to quite a bit of implementation effort.
Enter default interface methods.
Example: Default Interface Method:
public interface ItemProvider {
Collection<Item> getItems();
default getFirstItem() {
Collection<Item> items = getItems();
if (items == null || items.isEmpty()) {
return null;
}
return items.iterator().next();
}
}
Now, we can freely call getFirstItem()
and every concrete class will inherit our new default implementation.
"But wait a second!" the astute reader might exclaim. "That's all well and good for a generic getFirst method, but we've lost the performance benefits of random index access in our ArrayList implementation!" (For the sake of example, we are ignoring the fact that you could do an instanceof
check and then cast to List
).
Java gives you several options when implementing an interface with default methods. The tutorial documentation states:
When you extend an interface that contains a default method, you can do the following:
- Not mention the default method at all, which lets your extended interface inherit the default method.
- Redeclare the default method, which makes it abstract.
- Redefine the default method, which overrides it.
In other words, we are free to keep the custom implementation for ListItemProvider
above, which takes advantage of the List interface's random array access.
How Does This Help Me?
Revisiting our earlier Google trends data - why does this powerful new Java 8 feature get comparatively so little attention? In my opinion, the answer is that by and large, developers are API consumers; relatively few are API authors. They do not generally appear in a java developer's day-to-day coding life. However, default interface methods are an incredibly powerful tool that enable API authors to confer considerable benefit to consumers, even if they are frequently unaware of it.
For an example, we need look no further than the Java Collections API. The entire Java 8 premise of Stream support is built on two new default interface methods:
java.util.Collection.java:
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
Default interface methods enabled the java language authors to provide forward-compatible stream functionality on any collection implementation; one simply has to call stream()
on any collection instance.
The Magnificent New Map
Outside of Streams, to me, the biggest "win" for the every day java engineer is the vastly more powerful java.util.Map
interface. Since the launch of the Collections API in Java 1.2 (ca. 1998) the built-in Map functionality has left a little bit to be desired. How many times have we written these lines of code, for example:
Example: Default Values (Java < 8):
String value = map.get(key);
if (value == null) {
value = DEFAULT_VALUE;
}
Code like this has become so idiomatic that it's almost unnoticeable (or we have worked around it using third-party helper libraries such as Google Guava or Apache Commons Collection Utils). With Java 8, however, we can finally write:
Example: Default Values (Java >= 8):
String value = map.getOrDefault(key, DEFAULT_VALUE);
And that's it. What a relief!
This barely scratches the surface, though, of the powerful new map functionality introduced with Java 8.
I have coded this idiom literally hundreds of times:
Example: Computing a Value (Java < 8):
List<Person> people = map.get(age);
if (people == null) {
people = new ArrayList<>();
map.put(people);
}
people.add(person);
Thanks to a simple default interface method you can now code with any Map implementation:
Example: Computing a Value (Java >= 8):
List<Person> people = map.computeIfAbsent(age, k -> ArrayList::new);
people.add(person);
This example combines a few Java 8 concepts so let's take a closer look. The full method signature is: default V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
. The second argument is a Function
(a type of FunctionalInterface
which takes one argument and returns a value) which is called if the map does not contain the specified key. Its argument is the key being referenced, and it returns the computed value to be mapped. In this example, the value does not actually depend on the key - it is always a new ArrayList()
so we can express that using a Java 8 method reference: ArrayList::new
.
Map iteration used to be this (bloated) idiom:
Example: Iterating over a Map (Java < 8):
Map<String, Collection<Person>> map = populateMap();
for (Map.Entry<String, Collection<Person>> entry : map.entrySet()) {
String lastName = entry.getKey();
Collection<Person> people = entry.getValue();
// ...
}
Using lambda's and the forEach()
default method we have a lean new implementation:
Example: Iterating over a Map (Java >= 8):
Map<String, Collection<Person>> map = populateMap();
map.forEach(k, v -> {
// ...
});
These are the three features I have found most useful, but additional powerful functionality abounds and can be explored by clicking on "Default Methods" in the Java 8 java.util.Map API. In particular, the compute()
and merge()
methods can be used to replace verbose idioms with clear and compact expressions.
And this is the key element that Java 8's features bring to the table: making it easier to write expressive code, making it easy to tell at a glance what a piece of functionality is meant to do. I look forward to continuing my exploration of Java 8 (and soon, Java 9!) at SourceClear, and to blog about its functionality again soon!