CodeGym /Cursos /JAVA 25 SELF /Métodos por defecto en interfaces

Métodos por defecto en interfaces

JAVA 25 SELF
Nivel 21 , Lección 2
Disponible

1. Introducción

Hace tiempo (antes de Java 8) una interfaz era muy estricta: en ella solo se podían declarar métodos abstractos (sin implementación) y constantes (public static final). Eso era cómodo hasta que apareció un gran problema: el desarrollo de las bibliotecas.

Imagina la situación

Has desarrollado una biblioteca popular que tiene una interfaz:

public interface Movable {
    void move(int x, int y);
}

Miles de programadores de todo el mundo escriben sus clases que implementan esta interfaz. Al cabo de un par de años te das cuenta de que a todos les falta el método reset(), que devuelve el objeto a su posición inicial. Lo añades a la interfaz:

public interface Movable {
    void move(int x, int y);
    void reset();
}

Y empieza el apocalipsis: ¡todos los proyectos que usan tu interfaz dejan de compilar! Ahora están obligados a implementar el método nuevo, y nadie contaba con ello. La migración se convierte en una pesadilla.

¡Los métodos por defecto son la solución!

Java 8 introdujo los métodos por defecto: ¡ahora puedes añadir un método con implementación directamente en la interfaz! Todas las clases antiguas reciben automáticamente una implementación estándar y su código no se rompe. Y si quieres, puedes sobrescribir el método a tu manera.

2. Sintaxis de los métodos por defecto

Un método por defecto es un método normal con implementación dentro de una interfaz, marcado con la palabra clave default.

public interface Movable {
    void move(int x, int y);

    default void reset() {
        // Implementación típica: volver al origen de coordenadas
        move(0, 0);
    }
}

Explicación:

  • Todos los métodos de una interfaz son por defecto public y abstract, pero los métodos por defecto no son abstractos: tienen cuerpo.
  • La palabra clave default siempre se escribe antes del tipo de retorno del método.

¿Cómo se ve en una clase?

public class Robot implements Movable {
    private int x, y;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("Robot movido a (" + x + ", " + y + ")");
    }

    // No es obligatorio implementar reset(): ¡funcionará la versión por defecto!
}

Ahora, si llamamos a reset() en un objeto Robot, se ejecutará la implementación de la interfaz Movable:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // Robot movido a (10, 20)
        robot.reset();      // Robot movido a (0, 0)
    }
}

3. Métodos por defecto en la biblioteca estándar

Los métodos por defecto se añadieron precisamente para poder evolucionar las enormes interfaces estándar de Java sin romper el código existente.

Ejemplo: interfaz List (Java 8+)

En Java 8 se añadieron a la interfaz List métodos con implementación, por ejemplo, forEach, replaceAll, sort:

default void forEach(Consumer<Entity> action) {
    for (Entity e : this) {
        action.accept(e);
    }
}

Si implementas tu propia lista y no sobrescribes forEach, seguirá funcionando gracias al método por defecto.

Aprenderás más sobre tipos genéricos (Consumer<Entity>) en el nivel 26 :P

4. ¿Para qué sirven los métodos por defecto?

  • Evolución del API sin romper el código: se pueden añadir métodos nuevos a una interfaz sin necesidad de implementarlos en todas las clases existentes.
  • Patrones de comportamiento reutilizables: puedes declarar un comportamiento por defecto para que las clases lo usen o lo sobrescriban.
  • Menos duplicación: si el comportamiento es igual para la mayoría de implementaciones, no hace falta copiar el mismo código en cada clase.

Analogía

Imagina que tienes un contrato de alquiler de un piso (una interfaz). Antes ponía: «El inquilino debe pagar el agua». Luego añadisteis: «El inquilino debe pagar la electricidad». Si no existieran los métodos por defecto, ¡tendrías que reescribir todos los contratos con todos los inquilinos! Con los métodos por defecto, simplemente añadís la cláusula y, si alguien lo necesita, puede acordar otra cosa.

5. Limitaciones y particularidades de los métodos por defecto

Los métodos por defecto no pueden sobrescribir métodos de la clase Object

No puedes declarar en una interfaz un método por defecto con una firma que coincida con equals, hashCode o toString de la clase Object. Es una protección contra la confusión: cualquier objeto en Java ya tiene esos métodos.

// ¡Error de compilación!
interface Broken {
    default boolean equals(Object obj) { return false; }
}

Conflictos de métodos por defecto

¿Qué pasa si una clase implementa dos interfaces que tienen cada una un método por defecto con la misma firma? El compilador de Java te dirá honestamente: «Decídelo tú, ¡no sé qué hacer!»

interface A {
    default void hello() { System.out.println("Hello from A"); }
}

interface B {
    default void hello() { System.out.println("Hello from B"); }
}

class C implements A, B {
    // Es obligatorio resolver el conflicto:
    @Override
    public void hello() {
        // Puedes elegir qué método invocar o implementar el tuyo propio
        A.super.hello(); // o B.super.hello();
    }
}

Si no implementas hello() en la clase C, habrá un error de compilación.

Los métodos por defecto pueden llamar a otros métodos de la interfaz

Un método por defecto puede invocar otros métodos de la interfaz, incluso abstractos. Lo importante es que exista una implementación en la clase.

interface Printer {
    void print(String text);

    default void printTwice(String text) {
        print(text);
        print(text);
    }
}

6. Ejemplo: evolucionamos una aplicación con un método por defecto

Veamos un ejemplo de uso de métodos por defecto en la interfaz Movable:

public interface Movable {
    void move(int x, int y);

    default void reset() {
        move(0, 0);
    }
}

Y tenemos la clase Robot, que implementa esta interfaz:

public class Robot implements Movable {
    private int x = 5;
    private int y = 7;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("Robot movido a (" + x + ", " + y + ")");
    }

    // No implementamos reset(): ¡usamos el método por defecto!
}

Ahora probemos a invocar ambos métodos:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // Robot movido a (10, 20)
        robot.reset();      // Robot movido a (0, 0)
    }
}

Si queremos que Robot se reinicie de una forma especial, basta con sobrescribir reset() en la clase:

@Override
public void reset() {
    System.out.println("¡El robot se apaga y vuelve a la base!");
    move(0, 0);
}

7. Métodos por defecto y la implementación múltiple de interfaces

Los métodos por defecto son especialmente útiles cuando una clase implementa varias interfaces. Pero hay un matiz: si ambas interfaces tienen un método por defecto con la misma firma, el compilador exigirá resolver el conflicto explícitamente.

Ejemplo de conflicto

interface A {
    default void show() { System.out.println("A"); }
}
interface B {
    default void show() { System.out.println("B"); }
}
class C implements A, B {
    @Override
    public void show() {
        // Elegimos explícitamente qué método por defecto usar
        A.super.show(); // o B.super.show();
    }
}

8. Esquema: cómo funciona la llamada a un método por defecto


+-------------------+
|   Movable         |
|-------------------|
| +move(int, int)   | <- método abstracto
| +reset()          | <- método por defecto
+-------------------+
         ^
         |
+-------------------+
|   Robot           |
|-------------------|
| +move(int, int)   | <- lo implementa
|                   | (no implementa reset())
+-------------------+
         |
     Llamada a reset()
         |
   Se utiliza la implementación
   de la interfaz Movable
Llamada a un método por defecto: implementación por defecto desde la interfaz

9. Errores típicos al trabajar con métodos por defecto

Error n.º 1: intentar declarar un método por defecto sin implementación.
¡Un método por defecto debe tener cuerpo! Si escribes default void foo();, el compilador te dirá enseguida: «¿Has olvidado las llaves?»

Error n.º 2: conflicto de métodos por defecto de distintas interfaces.
Si una clase implementa dos interfaces con el mismo método por defecto, debes resolver el conflicto explícitamente; de lo contrario, el compilador no te permitirá compilar el código.

Error n.º 3: intentar declarar un método por defecto con la firma de un método de Object.
No se pueden hacer métodos por defecto equals, hashCode o toString en una interfaz: solo métodos abstractos con esos nombres.

Error n.º 4: olvidar que los métodos por defecto no son «magia», sino una herramienta práctica.
Los métodos por defecto no anulan el principio de que una interfaz es un contrato. Si el comportamiento por defecto no encaja, sobrescribe siempre el método por defecto en la clase.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION