Métodos static y default en Interfaces en Java
1. Introducción
Java 8 introdujo algunas características completamente nuevas, incluyendo expresiones lambda, interfaces funcionales, referencias de métodos, flujos, Optional y métodos static y default en interfaces.
Algunas de estas características las cubriremos en otros artículos. No obstante, los métodos static y default en interfaces merecen un análisis más profundo por sí mismos.
En este tutorial, aprenderemos cómo utilizar los métodos static y default en interfaces y discutiremos algunas situaciones en las que pueden ser útiles.
2. Por Qué las Interfaces Necesitan Métodos default
Al igual que los métodos regulares de las interfaces, los métodos default son implícitamente públicos; no es necesario especificar el modificador public.
A diferencia de los métodos regulares de las interfaces, los declaramos con la palabra clave default al principio de la firma del método y proporcionan una implementación.
Veamos un ejemplo sencillo:
public interface MiInterfaz {
// métodos regulares de la interfaz
default void metodoPredeterminado() {
// implementación del método default
}
}
La razón por la cual la versión Java 8 incluyó métodos default es bastante obvia.
En un diseño típico basado en abstracciones, donde una interfaz tiene una o múltiples implementaciones, si se agregan uno o más métodos a la interfaz, todas las implementaciones también se verán obligadas a implementarlos. De lo contrario, el diseño simplemente se romperá.
Los métodos default de las interfaces son una forma eficiente de abordar este problema. Nos permiten agregar nuevos métodos a una interfaz que están automáticamente disponibles en las implementaciones. Por lo tanto, no necesitamos modificar las clases que las implementan.
De esta manera, se conserva la compatibilidad hacia atrás sin tener que refactorizar los implementadores.
3. Métodos default de la Interfaz en Acción
Para comprender mejor la funcionalidad de los métodos default de la interfaz, creemos un ejemplo sencillo.
Supongamos que tenemos una interfaz Vehículo
ingenua y solo una implementación. Podría haber más, pero mantengámoslo simple:
public interface Vehiculo {
String obtenerMarca();
String acelerar();
String desacelerar();
default String activarAlarma() {
return "Activando la alarma del vehículo.";
}
default String desactivarAlarma() {
return "Desactivando la alarma del vehículo.";
}
}
Ahora escribamos la clase que implementa:
public class Coche implements Vehiculo {
private String marca;
// constructores/getters
@Override
public String obtenerMarca() {
return marca;
}
@Override
public String acelerar() {
return "El coche está acelerando.";
}
@Override
public String desacelerar() {
return "El coche está desacelerando.";
}
}
Finalmente, definamos una clase main
típica, que crea una instancia de Coche
y llama a sus métodos:
public static void main(String[] args) {
Vehiculo coche = new Coche("Aston Martin");
System.out.println(coche.obtenerMarca());
System.out.println(coche.acelerar());
System.out.println(coche.desacelerar());
System.out.println(coche.activarAlarma());
System.out.println(coche.desactivarAlarma());
}
Por favor, ten en cuenta cómo los métodos default, activarAlarma()
y desactivarAlarma()
, de nuestra interfaz Vehiculo
están automáticamente disponibles en la clase Coche
.
Además, si en algún momento decidimos agregar más métodos default a la interfaz Vehiculo, 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 común de los métodos default de la interfaz es proporcionar incrementalmente funcionalidad adicional a un tipo dado sin descomponer las clases que la implementan.
Además, podemos usarlos para proporcionar funcionalidad adicional en torno a un método abstracto existente:
public interface Vehiculo {
// métodos adicionales de la interfaz
double obtenerVelocidad();
default double obtenerVelocidadEnKMH(double velocidad) {
// conversión
}
}
4. Reglas de Herencia de Múltiples Interfaces
Los métodos default de la interfaz son una característica bastante buena, pero hay 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 default.
Para comprender mejor este escenario, definamos una nueva interfaz Alarma
y refactoricemos la clase Coche
:
public interface Alarma {
default String activarAlarma() {
return "Activando la alarma.";
}
default String desactivarAlarma() {
return "Desactivando la alarma.";
}
}
Con esta nueva interfaz que define su propio conjunto de métodos default, la clase Coche
implementaría tanto Vehiculo
como Alarma
:
public class Coche implements Vehiculo, Alarma {
// ...
}
En este caso, el código simplemente no se compilará, ya que hay un conflicto causado por la herencia de múltiples interfaces (también conocido como el Problema del Diamante). La clase Coche
heredaría ambos conjuntos de métodos default. Entonces, ¿cuáles deberíamos llamar?
Para resolver esta ambigüedad, debemos proporcionar explícitamente una implementación para los métodos:
@Override
public String activarAlarma() {
// implementación personalizada
}
@Override
public String desactivarAlarma() {
// implementación personalizada
}
También podemos hacer que nuestra clase utilice los métodos default de una de las interfaces.
Veamos un ejemplo que utiliza los métodos default de la interfaz Vehiculo
:
@Override
public String activarAlarma() {
return Vehiculo.super.activarAlarma();
}
@Override
public String desactivarAlarma() {
return Vehiculo.super.desactivarAlarma();
}
De manera similar, podemos hacer que la clase utilice los métodos default definidos dentro de la interfaz Alarma
:
@Override
public String activarAlarma() {
return Alarma.super.activarAlarma();
}
@Override
public String desactivarAlarma() {
return Alarma.super.desactivarAlarma();
}
Incluso es posible hacer que la clase Coche
utilice ambos conjuntos de métodos default:
@Override
public String activarAlarma() {
return Vehiculo.super.activarAlarma() + " " + Alarma.super.activarAlarma();
}
@Override
public String desactivarAlarma() {
return Vehiculo.super.desactivarAlarma() + " " + Alarma.super.desactivarAlarma();
}
5. Métodos static de la Interfaz
Además de declarar métodos default en las interfaces, Java 8 también nos permite definir e implementar métodos static en las interfaces.
Dado que los métodos static no pertenecen a un objeto en particular, no forman parte de la API de las clases que implementan la interfaz; por lo tanto, deben ser llamados usando el nombre de la interfaz precediendo el nombre del método.
Para comprender cómo funcionan los métodos static en las interfaces, refactoricemos la interfaz Vehiculo
y agreguemos un método de utilidad static a ella:
public interface Vehiculo {
// métodos regulares / predeterminados de la interfaz
static int obtenerPotencia(int rpm, int torque) {
return (rpm * torque) / 1337;
}
}
Definir un método static dentro de una interfaz es idéntico a definirlo en una clase. Además, un método static puede ser invocado dentro de otros métodos static y default.
Supongamos que queremos calcular la potencia de un motor de un vehículo dado. Solo llamamos al método obtenerPotencia():
Vehiculo.obtenerPotencia(1000, 400));
La idea detrás de los métodos static de la interfaz es proporcionar un mecanismo simple que nos permita aumentar el grado de cohesión de un diseño reuniendo métodos relacionados en un solo lugar sin tener que crear un objeto.
Lo mismo se puede hacer con clases abstractas. La principal diferencia es que las clases abstractas pueden tener constructores, estado y comportamiento.
Además, los métodos static en las interfaces permiten agrupar métodos de utilidad relacionados sin tener que crear clases de utilidad artificiales que sean simplemente marcadores de posición para métodos static.
6. Conclusión
En este artículo, exploramos a fondo el uso de métodos static y default de la interfaz en Java 8. A primera vista, esta característica puede parecer un poco desordenada, especialmente desde la perspectiva de un purista orientado a objetos. Idealmente, las interfaces no deberían encapsular comportamiento, y solo deberíamos usarlas para definir la API pública de un cierto tipo.
Sin embargo, cuando se trata de mantener la compatibilidad hacia atrás con código existente, los métodos static y default son un buen compromiso.