Overview
In Java, an interface is a contract that has methods declared in it. Those are abstract (just the method name, parameter names and types and the return type). The class that is implementing the interface must provide an implementation for all the methods in the interface. Or, a class might inherit the implementation of an interface from a superclass.
Adding a new method to an interface is a backward-incompatible or a breaking change before Java 8. If we add a new method to an interface, all the existing concrete implementations will break and fail to compile. But since Java 8 there has been default methods in Java. This allows us to add a new method to an interface without breaking the existing classes or clients.
This post dives deep on default methods in Java 8+ with examples. We will also look at examples from the JDK library itself. In fact, it is this feature that enabled the developers of Java to provide some great utilities in Java 8.
What is an Interface
Interfaces form the API (Application Programming Interface). The API specifies the operations that can be performed. It defines what the interface is for without specifying any implementation detail. There can be multiple implementations that satisfy the contract of an interface in many ways.
Example: Look at the List interface in Java. It has the add method, get method, contains method etc,. They represent a contract i.e., what a List will do and what it will offer. Here, at a high-level, a list is an ordered collection that allows us to add elements into it, get element at an index, remove elements etc,.
An interface has no implementation detail. They do not say how to implement to achieve the above-stated functionalities. But sometimes a contract is made generic and will let the implementing classes to specify the exact behaviour. Example: the external appearing behaviour when there are multiple threads performing an action.
Interfaces before Java 8 - Set in stone?
A class implementing an interface must provide an implementation for all the methods in the interface (unless we mark the class abstract). As I alluded to in the overview section, any new method that we add to an interface will break the existing classes.
APIs are evolving
Imagine you are the author of an awesome API and there are tens of clients who are using it and many have their own implementation. You are looking to add a few more methods to your interface. You can update the classes that ship with your library when you make the interface upgrade. But what about the classes that the clients have written implementing your interface? Who would update them when they start to depend on your new evolved interface?
Thus, before Java 8, any interface change must have all the implementing classes updated at the same time. This is not flexible and sometimes even possible. The developers of the JDK saw this as a major hindrance to provide useful functionalities in the Java library. This led to the introduction of a new language feature - the default methods.
What are default methods
Quoting from the Default Methods Oracle documentation
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.
When we want to add a new method to an interface, we can add it as a default method. This will enable the old classes written against our old interface continue to compile and run even when the new updated interface is deployed to the client. Sounds awesome!!
Default methods in Java - An Example
Let us say we have our own awesome collection interface like
public interface MyAwesomeCollection<T> {
List<T> getItems();
void addItem(T item);
void addItems(List<T> items);
}
It allows to add one item or multiple items of any type (a generic interface like a List). It has a method, getItems, that returns all the items in our awesome collection. Our clients have already implemented their own implementations of MyAwesomeCollection.
Some time later, we see that there is no functionality to find out the size of the collection (size method) or if our collection is empty (isEmpty method). We can add those methods to the interface, but we already know how bad this can be. Since this is a breaking change, we have to roll out our library using a new major version (like 2.0 from 1.0). Then, the clients have to update their code when they depend on the newer version of the library.
Not so when we have default methods. In Java, we can add the above methods to the interface and can provide an implementation right in the interface itself!!. This is very new to what we have seen over the years. An interface can have an implementation. Let us add default methods to MyAwesomeCollection.
public interface MyAwesomeCollection<T> {
List<T> getItems();
void addItem(T item);
void addItems(List<T> items);
default int size() {
return getItems().size();
}
default boolean isEmpty() {
return size() == 0;
}
}
We create a default method by adding the default keyword before the return type in the interface. Then, we provide the method body right in the interface itself.
Now, when we roll out the library, the client can now start to use the size() and the isEmpty() methods whenever they want.
Note: This is only a default implementation. The clients can still override the size() and the isEmpty() methods just like the other methods.
Thus, from the callers standpoint, they do not know if a method is abstract, and the implementation is present in the implementing code or it is provided as a default method. When we call them as
MyAwesomeCollection MyAwesomeCollection = new MyAwesomeCollectionImpl()
MyAwesomeCollection.getItems();
MyAwesomeCollection.size();
MyAwesomeCollection.isEmpty();
Calling size() and isEmpty() is same as calling getItems(). Thus, both the implementing classes and the callers will not notice anything different and require no change.
Default method implementations need not always be the great
Providing a default implementation may not always be possible or be the best option.
In the above example, assume instead of getItems we had a method called getIterator that returns an Iterator over the collection rather than returning the collection itself.
public interface MyAwesomeCollection<T> {
Iterator<T> getIterator();
void addItem(T item);
void addItems(List<T> items);
}
To provide a default implementation for the size method, we have to get the Iterator and iterate over the elements to find the size of the collection. This is not optimal.
default int size() {
Iterator<T> iterator = getIterator();
int count = 0;
while (iterator.hasNext()) {
iterator.next();
count++;
}
return count;
}
If the subclasses override the size method, it might be (and should be) able to return the size of the collection (stored as an instance variable). In such situations, it is a decision that we have to make whether to provide a default implementation or not.
Throwing an exception as a default implementation
If we don’t want to provide a default implementation and at the same time, don’t want to break clients by adding a new method, we can provide a default implementation that throws an Exception. Usually it is UnsupportedOperationException
default void size() {
throw new UnsupportedOperationException("Getting the size is not supported");
}
Then, after we roll out our library, the clients can override this method and provide a valid implementation whenever they want.
This is what the Java Iterator’s remove() method does.
If you would like to know about Iterators in Java, take a look at the following posts.
Example #2 - A Walking Game Character
I would like to take a show another example interface, evolve it by adding a new method, provide a default method that would break its overall interface contract.
We have a GameCharacter interface. A character when it starts will be at position 0. There are methods that would make the game character walk. Our character has a stride length which is the length of step it takes when walking.
The getNumberOfStepsMade returns the number of steps the game character has made so far (from when the object was created).
public interface GameCharacter {
/**
* The current location of the game character.
* @return The current location.
*/
int getLocation();
/**
* Get the current stride length of the game character.
* @return the game character's stride length when walking one step.
*/
int getStrideLength();
/**
* Set the stride length of the game character.
* @param strideLength The new stride length.
*/
void setStrideLength(int strideLength);
/**
* Make the game character take one step.
* Each step will be of length equal to the stride length.
*/
void walk();
/**
* Make the game character take the provided number of steps.
* Each step will be of length equal to the stride length.
* @param numberOfSteps the number of steps to walk.
*/
void walk(int numberOfSteps);
/**
* Get the total number of steps made by the game character.
* @return the total number of steps made by the game character.
*/
int getNumberOfStepsMade();
}
Here’s a simple implementation of the GameCharacter
public class GameCharacterImpl implements GameCharacter {
private int strideLength = 1; //default stride length.
private int location = 0; //start at 0.
private int numberOfStepsMade = 0;
@Override
public int getLocation() {
return location;
}
@Override
public void setStrideLength(int strideLength) {
this.strideLength = strideLength;
}
@Override
public int getStrideLength() {
return strideLength;
}
@Override
public void walk() {
walk(1);
}
@Override
public void walk(int numberOfSteps) {
location += numberOfSteps * strideLength;
numberOfStepsMade += numberOfSteps;
}
@Override
public int getNumberOfStepsMade() {
return numberOfStepsMade;
}
}
Let us see this in action.
//Create a new object
GameCharacter gameCharacter = new GameCharacterImpl();
System.out.println(gameCharacter.getLocation()); //0
gameCharacter.walk();
System.out.println(gameCharacter.getLocation()); //1
gameCharacter.walk(2);
System.out.println(gameCharacter.getLocation()); //3
gameCharacter.setStrideLength(2);
gameCharacter.walk(2);
System.out.println(gameCharacter.getLocation()); //7
System.out.println(gameCharacter.getNumberOfStepsMade()); //5
Our character starts at location 0 and with stride length of 1. When we make it walk, it moves to location 1. We change the stride length to 2 and make it walk 2 steps. The output should be self-explanatory. In total, it has made 5 steps.
Example #2 cont. - A default method that can break the contract
Sometime later we want to add new method called chargedWalk. When we call this the game character’s actual stride length does not change. But it will walk with 2x its stride length.
How can we provide a default implementation?
Rather than walking with 2x stride length, we can make it walk 2x the number of desired steps with the same stride length. Sounds great, isn’t it?
default void chargedWalk() {
chargedWalk(1);
}
default void chargedWalk(int numberOfSteps) {
walk(numberOfSteps * 2);
}
The chargedWalk calls walk() method with two times the numberOfSteps desired to be taken.
gameCharacter = new GameCharacterImpl();
gameCharacter.setStrideLength(2);
gameCharacter.chargedWalk(3);
System.out.println(gameCharacter.getLocation()); //12
System.out.println(gameCharacter.getNumberOfStepsMade()); //6
We create a game character and set its default stride length to 2 and make it do a charged walk for 3 steps. It must end up at 12. That is what the getLocation() returns.
But there is a problem. The getNumberOfStepsMade() must return 3, but it returns 6. This is because we made it to walk 2 times more than the numberOfSteps to arrive at the correct final position. This default implementation is not correct as it results in a wrong overall state (correct final location, but a wrong numberOfStepsMade).
Example #2 cont. - An Alternate implementation
We could temporarily change the stride length to be 2 times that of the current stride length and the make the character walk. Once done, we can restore the stride length to its previous value.
default void chargedWalk(int numberOfSteps) {
int currentStrideLength = getStrideLength();
setStrideLength(currentStrideLength * 2);
walk(numberOfSteps);
setStrideLength(currentStrideLength);
}
gameCharacter = new GameCharacterImpl();
gameCharacter.setStrideLength(2);
gameCharacter.chargedWalk2(3);
System.out.println(gameCharacter.getLocation());//12
System.out.println(gameCharacter.getNumberOfStepsMade());//3
As I said earlier, whether this implementation is acceptable depends on the situation and use case - is it acceptable to change the stride length temporarily? What if it is a concurrent application and there are multiple threads? Some thread could call getStrideLength() method when chargedWalk() is in progress. If it does, it can notice a wrong stride length.
Summary: It is the responsibility of the interface owner to decide what the default logic of a new method should be. If nothing is valid or reasonable, throw an UnsupportedOperationException.
Default methods in the JDK
Let us look at how default methods have been helpful in the introduction of new methods in Java 8.
List sort
We have been calling sort() method on a list by passing a comparator to sort a list in Java. The sort() method in the List interface was added in Java 8.
list.sort(Comparator.naturalOrder());
It is the default methods that enabled the addition of such a useful method to the List Interface without requiring change to all the hundreds of implementation of the List interface.
Comparator
The Comparator interface has a lot of default methods like reversed() and thenComparing() methods. The reversed is used to get a Comparator that does the comparisons in the reverse order of the original comparator.
List<String> list = new ArrayList<>();
list.add("apple");
list.add("orange");
list.add("pear");
Comparator<String> lengthComparator = Comparator.comparingInt(String::length);
//Compare by string length - from shortest to longest
list.sort(lengthComparator);
System.out.println(list); //[pear, apple, orange]
//Reverse order
list = new ArrayList<>();
list.sort(lengthComparator.reversed());
System.out.println(list); //[orange, apple, pear]
The thenComparing methods are explained in the Comparator Comparing post.
You can find a lot of default methods when you navigate the Collection libraries or JDK in general.
Conclusion
This post covered one of the most important changes to the Java language introduced - the default methods in Java 8. It enables us to evolve the APIs (interfaces) without affecting the existing implementations. We saw examples of how to write a default method in Java. We also looked at cases where a having an implementation within a default method may not be the best option and instead that we can throw an exception and let the subclasses override it when they need it. Last, we looked at some examples of default methods used within the Java libraries.