1. Información general
Java 8 trajo a la mesa algunas características nuevas, incluidas expresiones lambda, interfaces funcionales, referencias de métodos, flujos, métodos opcionales y estáticos y predeterminados en interfaces.
Algunos de ellos ya se han tratado en este artículo. No obstante, los métodos estáticos y predeterminados en las interfaces merecen una mirada más profunda por sí mismos.
En este artículo, analizaremos en profundidad cómo usar métodos estáticos y predeterminados en interfaces y analizaremos algunos casos de uso en los que pueden ser útiles.
2. Por qué se necesitan métodos predeterminados en las interfaces
Al igual que los métodos de interfaz normales, los métodos predeterminados son implícitamente públicos ; no es necesario especificar el modificador público .
A diferencia de los métodos de interfaz normales, se declaran con la palabra clave predeterminada al comienzo de la firma del método y proporcionan una implementación .
Veamos un ejemplo sencillo:
public interface MyInterface { // regular interface methods default void defaultMethod() { // default method implementation } }
La razón por la que se incluyeron métodos predeterminados en la versión de Java 8 es bastante obvia.
En un diseño típico basado en abstracciones, donde una interfaz tiene una o varias implementaciones, si se agregan uno o más métodos a la interfaz, todas las implementaciones se verán obligadas a implementarlas también. De lo contrario, el diseño simplemente se romperá.
Los métodos de interfaz predeterminados son una forma eficaz de solucionar este problema. Ellos nos permiten agregar nuevos métodos a una interfaz que están disponibles de forma automática en las implementaciones . Por tanto, no es necesario modificar las clases de implementación.
De esta manera, la compatibilidad con versiones anteriores se conserva prolijamente sin tener que refactorizar los implementadores.
3. Métodos de interfaz predeterminados en acción
Para comprender mejor la funcionalidad de los métodos de interfaz predeterminados , creemos un ejemplo simple.
Digamos que tenemos una interfaz de vehículo ingenua y solo una implementación. Podría haber más, pero hagámoslo así de simple:
public interface Vehicle { String getBrand(); String speedUp(); String slowDown(); default String turnAlarmOn() { return "Turning the vehicle alarm on."; } default String turnAlarmOff() { return "Turning the vehicle alarm off."; } }
Y escribamos la clase de implementación:
public class Car implements Vehicle { private String brand; // constructors/getters @Override public String getBrand() { return brand; } @Override public String speedUp() { return "The car is speeding up."; } @Override public String slowDown() { return "The car is slowing down."; } }
Por último, definamos una clase principal típica , que crea una instancia de Car y llama a sus métodos:
public static void main(String[] args) { Vehicle car = new Car("BMW"); System.out.println(car.getBrand()); System.out.println(car.speedUp()); System.out.println(car.slowDown()); System.out.println(car.turnAlarmOn()); System.out.println(car.turnAlarmOff()); }
Observe cómo los métodos predeterminados turnAlarmOn () y turnAlarmOff () de nuestra interfaz de vehículo están disponibles automáticamente en la clase de automóvil .
Además, si en algún momento decidimos agregar más métodos predeterminados a la interfaz del vehículo , la aplicación seguirá funcionando y no tendremos que obligar a la clase a proporcionar implementaciones para los nuevos métodos.
El uso más típico de los métodos predeterminados en las interfaces es proporcionar de forma incremental funcionalidad adicional a un tipo determinado sin desglosar las clases de implementación.
Además, se pueden utilizar para proporcionar funcionalidad adicional en torno a un método abstracto existente :
public interface Vehicle { // additional interface methods double getSpeed(); default double getSpeedInKMH(double speed) { // conversion } }
4. Reglas de herencia de múltiples interfaces
Los métodos de interfaz predeterminados son una característica bastante agradable, pero con algunas advertencias que vale la pena mencionar. Dado que Java permite que las clases implementen múltiples interfaces, es importante saber qué sucede cuando una clase implementa varias interfaces que definen los mismos métodos predeterminados .
Para comprender mejor este escenario, definamos una nueva interfaz de alarma y refactoricemos la clase Car :
public interface Alarm { default String turnAlarmOn() { return "Turning the alarm on."; } default String turnAlarmOff() { return "Turning the alarm off."; } }
Con esta nueva interfaz definiendo su propio conjunto de métodos predeterminados , la clase Car implementaría tanto Vehículo como Alarma :
public class Car implements Vehicle, Alarm { // ... }
En este caso, el código simplemente no se compilará, ya que existe un conflicto causado por la herencia de múltiples interfaces (también conocido como el Problema del Diamante). La clase Car heredaría ambos conjuntos de métodos predeterminados . ¿Cuáles deberían llamarse entonces?
Para resolver esta ambigüedad, debemos proporcionar explícitamente una implementación para los métodos:
@Override public String turnAlarmOn() { // custom implementation } @Override public String turnAlarmOff() { // custom implementation }
También podemos hacer que nuestra clase use los métodos predeterminados de una de las interfaces .
Veamos un ejemplo que usa los métodos predeterminados de la interfaz del vehículo :
@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff(); }
De manera similar, podemos hacer que la clase use los métodos predeterminados definidos dentro de la interfaz de alarma :
@Override public String turnAlarmOn() { return Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Alarm.super.turnAlarmOff(); }
Además, incluso es posible hacer que la clase Car use ambos conjuntos de métodos predeterminados :
@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn() + " " + Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff() + " " + Alarm.super.turnAlarmOff(); }
5. Métodos de interfaz estática
Además de poder declarar métodos predeterminados en interfaces, Java 8 nos permite definir e implementar métodos estáticos en interfaces .
Since static methods don't belong to a particular object, they are not part of the API of the classes implementing the interface, and they have to be called by using the interface name preceding the method name.
To understand how static methods work in interfaces, let's refactor the Vehicle interface and add to it a static utility method:
public interface Vehicle { // regular / default interface methods static int getHorsePower(int rpm, int torque) { return (rpm * torque) / 5252; } }
Defining a static method within an interface is identical to defining one in a class. Moreover, a static method can be invoked within other static and default methods.
Now, say that we want to calculate the horsepower of a given vehicle's engine. We just call the getHorsePower() method:
Vehicle.getHorsePower(2500, 480));
The idea behind static interface methods is to provide a simple mechanism that allows us to increase the degree of cohesion of a design by putting together related methods in one single place without having to create an object.
Pretty much the same can be done with abstract classes. The main difference lies in the fact that abstract classes can have constructors, state, and behavior.
Furthermore, static methods in interfaces make possible to group related utility methods, without having to create artificial utility classes that are simply placeholders for static methods.
6. Conclusion
En este artículo, exploramos en profundidad el uso de métodos de interfaz estáticos y predeterminados en Java 8. A primera vista, esta característica puede parecer un poco descuidada, particularmente desde una perspectiva purista orientada a objetos. Idealmente, las interfaces no deberían encapsular el comportamiento y deberían usarse solo para definir la API pública de un cierto tipo.
Sin embargo, cuando se trata de mantener la compatibilidad con versiones anteriores del código existente, los métodos estáticos y predeterminados son una buena compensación.
Y, como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.