The purpose of a default
method is to provide a default implementation for an interface method in case the classes implementing the interface do not implement the method.
Remember that a Java interface is like a contract. The classes (except the abstract ones) that implement the interface must implement all interface methods as a contract obligation, otherwise they do not compile.
In theory, there is no reason to implement a method in an interface, but there are some practical reasons, among which are:
Add features without breaking existing code
This is the case of the example quoted in the question, from the List
interface.
Before default
methods, if you wanted to add a common method to an interface you would potentially break the agreement and all classes implementing it would need to be modified.
In the case of List
, consider that it is one of the most used APIs, and several libraries and systems have their own implementations. This would be an impediment to migrating to the new version of Java until all dependencies and components were updated.
The main goal of default
methods is to enable APIs to evolve seamlessly, without fuss, since "old" code can use a newer API without having to be updated.
This is especially advantageous when the new method adds some functionality, but is not strictly necessary for "old" mode operation.
Of course, this does not solve the problem when a change in the operation of an API occurs, because in this case the existing methods would need to be modified and default
methods are not recommended.
In a simple example, imagine an interface of a DAO like this:
public interface ClienteDao {
void incluir(Cliente c);
}
Now imagine that this DAO has multiple implementations across multiple modules of a large system. Different teams are responsible for the different modules which in turn are put into production at different times.
In version 2.0 of the system, you add a new method in the interface, but you can not expect all other modules to be updated. You could do this:
public interface ClienteDao {
void incluir(Cliente c);
default void incluir(List<Cliente> clientes) {
for (Cliente c : clientes) incluir(c);
}
}
Okay, now the system API is up to date and everything keeps running.
While other teams do not implement the new method, the default version serves the purposes well. Probably not the most optimized thing in the world, but it works.
For an in-memory DAO, the default implementation is probably sufficient. When a team that implements DAO for a particular database is to upgrade its module, they can implement an optimized version to include batch clients.
Flexibility in code reuse
Reusing code via inheritance is not usually a good practice, but the reality is that having methods in interfaces enables a special type of multiple inheritance or even be considered a type of trait .
In Java 6 you could reuse methods from other classes using import static
to access static methods as if they were part of the class. But with the default
methods of Java 8 you can implement an interface to bring a set of methods to your class.
The advantage of this over import static
is that the methods are part of the class API and can be used polymorphically.
The advantage over delegating to other classes is that the code gets cleaner.
The advantage over inheritance is that you can "inherit" methods from multiple interfaces.
Incidentally, the entire Java API would be different if default
methods existed longer.
A simple example could be a more flexible generic DAO, usually implemented using an abstract class. If you have any experience with systems you know there are always exceptions and not all DAOs need to have all methods. This occurs, for example, in entities that are read-only.
Example:
public interface GenericDao<T> {
default Connection getConnection() {
return DbUtils.getConnection();
}
}
public interface ReadOnlyDao<T> extends GenericDao<T> {
T instanciar(ResultSet rs); //específico para cada entidade
default T recuperar(int id) {
Connetion c = getConnetion();
... executa SQL usando alguma mágica ...
return instanciar(rs);
}
default List<T> listarTodos() {
...
}
}
public interface WriteOnlyDao<T> extends GenericDao<T> {
void set(PreparedStatement st, T entidade); //coloca os parâmetros da entidade na query
default void incluir(T entidade) {
Connetion c = getConnetion();
... executa SQL usando alguma mágica ...
}
}
public interface DaoCompleto extends ReadOnlyDao, WriteOnlyDao {}
With these implementations, just implement the interfaces as needed:
public class FeriadosDao implements ReadOnlyDao { ... }
public class LogAcessoDao implements WriteOnlyDao { ... }
public class ClienteDao implements DaoCompleto { ... }
And right away, we can incorporate features in granular form as needed.