Métodos Default y Static en Interfaces de Java
Agrega métodos default y static a interfaces de Java para evolucionar APIs sin romper las implementaciones existentes.
Hasta Java 7, una interfaz solo podía declarar métodos abstractos — los cuerpos residían en las clases que la implementaban. Java 8 añadió tres cosas nuevas que puedes colocar dentro de una interfaz:
- Métodos
default— un método con cuerpo, heredado por los implementadores que no lo sobreescriben. - Métodos
static— métodos de utilidad que pertenecen a la interfaz misma. - Métodos
private(Java 9) — helpers compartidos entre los métodosdefaultystaticde la interfaz.
El caso de uso motivador fue la evolución de APIs. Una vez que java.util.Collection había existido durante quince años con millones de implementaciones, el equipo de Java quería agregar stream() sin romper ninguna de ellas. default Stream<E> stream() { ... } hizo exactamente eso.
Métodos default
Un método default tiene cuerpo. Las clases que implementan la interfaz lo heredan gratuitamente y pueden sobreescribirlo si desean un comportamiento diferente:
public interface Greeter {
String name();
default String greet() {
return "Hello, " + name() + "!";
}
}
public class English implements Greeter {
public String name() { return "Alice"; }
}
public class Loud implements Greeter {
public String name() { return "Bob"; }
public String greet() { return "HEY " + name().toUpperCase() + "!"; } // override
}
new English().greet(); // Hello, Alice!
new Loud().greet(); // HEY BOB!default es solo un marcador — la palabra clave le indica al compilador "esto es el cuerpo de un método dentro de una interfaz". Sin ella, un método de interfaz no tiene cuerpo.
Al igual que cualquier miembro de interfaz no private, un método default es implícitamente public; no puedes hacerlo protected ni de acceso de paquete. No hay palabra clave public en los ejemplos anteriores porque es la única opción que el compilador permite.
Agregar un método default a una interfaz existente es un cambio no rupturante. Los implementadores que no lo sobreescriben heredan el valor predeterminado. Esa es la propiedad que hace posible la evolución de interfaces.
Métodos static en interfaces
Un método static en una interfaz pertenece a la interfaz, no a las instancias. Se invoca a través del nombre de la interfaz:
public interface Path {
static Path of(String s) { return new SimplePath(s); }
String value();
}
Path p = Path.of("/tmp/foo");Un uso común son los métodos de fábrica que producen instancias de la interfaz — Path.of, List.of, Map.of, Stream.of. Permiten que los llamadores dependan de la interfaz incluso al construir, en lugar de tener que elegir una clase de implementación específica.
Los métodos estáticos de interfaz no se heredan. Siempre se invocan a través del nombre de la interfaz, nunca a través de una subclase.
Métodos private (Java 9+)
Un método private en una interfaz es un helper visible solo para los otros métodos de esa misma interfaz. Permite que dos métodos default compartan lógica sin exponer esa lógica a los implementadores:
public interface Logger {
default void info(String msg) { log("INFO", msg); }
default void warn(String msg) { log("WARN", msg); }
private void log(String level, String msg) {
System.out.println("[" + level + "] " + msg);
}
}Sin private, tendrías que copiar el cuerpo en ambos métodos default o exponer log como un método default en sí mismo — lo que permitiría a los implementadores sobreescribirlo, algo que probablemente no deseas.
Lo que los métodos default no pueden hacer
Los métodos default viven en la interfaz, que no tiene estado. Pueden invocar métodos abstractos y otros métodos default/static, pero no pueden leer ni escribir campos de instancia — no hay ninguno que leer.
public interface Counter {
int get();
void increment();
default void incrementTwice() { // ok — only calls abstract methods
increment();
increment();
}
}Esto limita cuánto comportamiento real puede albergar un método default. Son más adecuados para capas de conveniencia delgadas sobre los métodos abstractos, no para reemplazar la implementación por completo.
Conflictos en diamante
Cuando una clase implementa dos interfaces que ambas proveen un método default con la misma firma, la clase debe resolver el conflicto explícitamente:
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
@Override
public String hello() {
return A.super.hello() + B.super.hello(); // explicit pick
}
}A.super.hello() invoca el default de A; B.super.hello() invoca el de B. El compilador se niega a compilar class C implements A, B { } sin una sobreescritura — no hay un ganador automático. Esta es la respuesta de Java al "problema del diamante" en la herencia múltiple — cuando aparece, la clase tiene que tomar la decisión.
Una subclase también prevalece sobre un default heredado: si una superclase concreta provee hello(), ese gana sobre cualquier método default de una interfaz.
Cuándo agregar un método default
default es la herramienta correcta cuando:
- Quieres extender una interfaz existente, ampliamente implementada, sin romper a los implementadores.
- El nuevo método es genuinamente derivable a partir de los métodos abstractos existentes.
- El default es "obviamente correcto" — la mayoría de los implementadores estarán satisfechos con él.
Es la herramienta incorrecta cuando:
- Estás tentado a colocar comportamiento real basado en estado (usa una clase abstracta en su lugar).
- El default en realidad no es útil y todos los implementadores lo sobreescribirán de todos modos (déjalo abstracto).
Un ejemplo completo
Qué sigue
Las interfaces y las clases abstractas tratan sobre cómo se relacionan los tipos entre archivos. El siguiente subtema — clases anidadas — trata sobre cómo se relacionan los tipos dentro de un único archivo: clases declaradas dentro de otras clases, y los cuatro tipos que Java ofrece. Continúa con clases anidadas.