Cerraste el módulo 3 con una jerarquía sólida: Material y sus tres soportes, Empleado, Prestamo y una capa de presentación separada. La herencia te dio reutilización y polimorfismo, pero también una atadura: en Java una clase solo puede extender una clase. Y BiblioTech ya empieza a chocar con ese límite. Nexus Software quiere prestar también salas de reunión, que se reservan y se liberan igual que un libro, pero que no son materiales del catálogo: no tienen ISBN, ni título, ni multa por retraso. ¿Las metes en la jerarquía Material sabiendo que la relación "es un" es falsa? ¿Duplicas el código de reserva? Ninguna de las dos.

La respuesta es la interfaz: un contrato de comportamiento puro, sin estado, que cualquier clase puede firmar —y puede firmar varios a la vez—. Las interfaces son el mecanismo con el que la biblioteca estándar de Java está construida: Comparable, Runnable, List, Iterable, AutoCloseable. Aprenderlas bien es el paso que separa escribir clases de diseñar sistemas, porque una interfaz te permite programar contra lo que algo hace sin saber nunca lo que algo es.

Contenido

  1. Qué es una interfaz: un contrato sin estado
  2. Sintaxis e implements
  3. Implementación múltiple y el problema del diamante
  4. Una interfaz es un TIPO: polimorfismo por contrato
  5. Los miembros de una interfaz y sus modificadores implícitos
  6. Métodos default: evolucionar una API sin romper nada
  7. Conflicto de default y Interfaz.super.metodo()
  8. Métodos static en interfaces
  9. Métodos private en interfaces (Java 9)
  10. Interfaces marcadoras y @FunctionalInterface
  11. Diseñar con interfaces: contrato, inversión de dependencias y testabilidad
  12. Interfaz frente a clase abstracta: el avance
  13. BiblioTech: Prestable, Notificable y SalaReuniones
  14. Errores Comunes y Consejos
  15. Ejercicios

  1. Qué es una interfaz: un contrato sin estado

Una interfaz es una declaración de capacidades: una lista de operaciones que una clase se compromete a ofrecer, sin decir nada sobre cómo las cumple ni sobre qué datos guarda.

La palabra clave es contrato. Cuando escribes:

public interface Prestable {
    boolean prestar();
    boolean devolver();
    boolean estaDisponible();
    int getDiasPrestamo();
}

no estás escribiendo código que se ejecute. Estás escribiendo una promesa: "cualquier clase que se declare Prestable sabrá prestarse, devolverse, decir si está disponible y decir cuántos días dura su préstamo". Quien reciba un Prestable puede llamar a esos cuatro métodos con total seguridad, sin conocer la clase concreta que hay detrás.

Tres notas fundamentales, que conviene fijar desde el principio:

  • Una interfaz no tiene estado de instancia. No puede declarar campos que varíen por objeto. Puede declarar constantes, y eso es todo (apartado 5).
  • Una interfaz no se puede instanciar. new Prestable() no compila: no hay nada que construir. Lo que se instancia es una clase que la implementa.
  • La relación que expresa es "puede hacer", no "es un". Libro es un Material (herencia). Libro puede prestarse (interfaz). Esa distinción resuelve el 80 % de las dudas de diseño.
Clase Interfaz
Responde a la pregunta ¿Qué es esto? ¿Qué sabe hacer esto?
Aporta Estado + comportamiento Contrato (y comportamiento por defecto)
Relación con la subclase extends, una sola implements, tantas como quieras
Ejemplo del dominio Libro extends Material Libro implements Prestable
Ejemplo del JDK String extends Object String implements Comparable, CharSequence

  1. Sintaxis e implements

Una interfaz se declara en su propio fichero .java, con el mismo nombre, exactamente igual que una clase:

package com.nexussoftware.bibliotech.dominio;

/**
 * Contrato de todo aquello que Nexus Software puede prestar a un empleado:
 * materiales del catalogo, salas de reunion, equipos informaticos...
 */
public interface Prestable {

    /** @return true si la operacion ha tenido efecto. */
    boolean prestar();

    /** @return true si la operacion ha tenido efecto. */
    boolean devolver();

    /** @return true si el recurso esta libre ahora mismo. */
    boolean estaDisponible();

    /** @return dias que dura el prestamo de este recurso. */
    int getDiasPrestamo();
}

Fíjate en lo que no aparece: no hay public en los métodos (es implícito), no hay abstract (también implícito), y los métodos terminan en ; en lugar de { ... }, porque no tienen cuerpo.

Una clase firma el contrato con implements:

public class SalaReuniones implements Prestable {

    private final String  codigo;
    private final int     capacidad;
    private boolean       libre;

    public SalaReuniones(String codigo, int capacidad) {
        this.codigo    = codigo;
        this.capacidad = capacidad;
        this.libre     = true;
    }

    @Override
    public boolean prestar() {
        if (!libre) { return false; }
        libre = false;
        return true;
    }

    @Override
    public boolean devolver() {
        if (libre) { return false; }
        libre = true;
        return true;
    }

    @Override
    public boolean estaDisponible() { return libre; }

    @Override
    public int getDiasPrestamo() { return 1; }   // una sala se reserva por dia

    public String getCodigo()  { return codigo; }
    public int    getCapacidad() { return capacidad; }
}

Dos reglas de compilación que debes conocer:

  1. Hay que implementar TODOS los métodos abstractos de la interfaz. Si olvidas uno, el compilador dice SalaReuniones is not abstract and does not override abstract method getDiasPrestamo() in Prestable. La alternativa es declarar la clase abstract, y entonces la obligación pasa a sus subclases (lección 04-02).
  2. Los métodos implementados deben ser public. Como el método de la interfaz es implícitamente public, reducir la visibilidad al implementarlo es un error: attempting to assign weaker access privileges.

El @Override no es obligatorio, pero úsalo siempre: es la misma red de seguridad que aprendiste en 03-05, y aquí detecta que has escrito estaDisponible() con una n de más.

Una clase puede además extender una clase e implementar interfaces a la vez, y el orden en la declaración es fijo: primero extends, luego implements.

public class Libro extends Material implements Comparable<Libro> { ... }

  1. Implementación múltiple y el problema del diamante

Esta es la razón de ser de las interfaces. Una clase puede implementar cuantas interfaces quiera, separadas por comas:

public class Material implements Prestable, Notificable {
    // debe cumplir los dos contratos
}

Pero solo puede extender una clase. ¿Por qué esa asimetría? La respuesta se llama problema del diamante.

Imagina que Java permitiera herencia múltiple de clases y que existieran estas dos:

class RecursoFisico {
    protected int codigo = 100;
    public String localizar() { return "Estanteria " + codigo; }
}

class RecursoDigital {
    protected int codigo = 200;
    public String localizar() { return "Servidor " + codigo; }
}

// ESTO NO EXISTE EN JAVA:
class LibroHibrido extends RecursoFisico, RecursoDigital { }
classDiagram
    class Object
    class RecursoFisico {
        +codigo int
        +localizar() String
    }
    class RecursoDigital {
        +codigo int
        +localizar() String
    }
    class LibroHibrido
    Object <|-- RecursoFisico
    Object <|-- RecursoDigital
    RecursoFisico <|-- LibroHibrido
    RecursoDigital <|-- LibroHibrido

El dibujo tiene forma de rombo —de ahí el nombre— y plantea preguntas sin respuesta única:

  • hibrido.localizar() ¿ejecuta la versión física o la digital?
  • hibrido.codigo ¿vale 100 o 200? ¿O el objeto tiene dos campos codigo?
  • Si RecursoFisico y RecursoDigital heredan ambos de una misma clase base con estado, ¿ese estado se guarda una vez o dos?

Los lenguajes que permiten herencia múltiple (C++, por ejemplo) resuelven esto con reglas complejas: herencia virtual, calificación explícita del ámbito, orden de linealización. Java tomó la decisión opuesta en 1995: una sola superclase, y punto. La complejidad se elimina de raíz.

Las interfaces se libran del problema porque, en su forma original, no aportan estado ni implementación. Si diez interfaces declaran int getDiasPrestamo();, la clase implementadora escribe un solo cuerpo que satisface las diez a la vez. No hay ambigüedad posible: no hay dos versiones entre las que elegir, hay cero.

Conflicto Herencia múltiple de clases Implementación múltiple de interfaces
Estado duplicado Sí: dos campos codigo Imposible: no hay estado de instancia
Dos cuerpos para el mismo método Sí: ambigüedad No: la clase escribe el único cuerpo
Constructores en cadena ¿Cuál se ejecuta primero? Las interfaces no tienen constructor

Eso fue exactamente cierto hasta Java 8, cuando llegaron los métodos default y con ellos un pequeño diamante residual. Lo verás resuelto en el apartado 7.

  1. Una interfaz es un TIPO: polimorfismo por contrato

Este es el apartado que da valor real a todo lo anterior. Una interfaz, aunque no se pueda instanciar, es un tipo válido de Java: puedes declarar variables, parámetros, valores de retorno y arrays con ella.

Prestable recurso = new SalaReuniones("SALA-A", 12);   // upcasting a la interfaz
recurso.prestar();
System.out.println(recurso.estaDisponible());          // false

La variable recurso no sabe que hay una sala detrás. Solo conoce los cuatro métodos del contrato. Es exactamente el polimorfismo de 03-06 (el tipo declarado decide qué puedes llamar, el tipo real decide qué se ejecuta), pero ahora el tipo declarado no es una superclase, sino un contrato que clases sin ningún parentesco pueden firmar.

Y aquí está el beneficio, en un método que sirve para todo el inventario de Nexus Software:

/** Informe de disponibilidad valido para libros, revistas, DVDs y salas. */
public static void informarDisponibilidad(Prestable[] recursos) {
    int libres = 0;
    for (Prestable p : recursos) {
        System.out.printf("  %-12s plazo %2d dias  %s%n",
                          p.getClass().getSimpleName(),
                          p.getDiasPrestamo(),
                          p.estaDisponible() ? "LIBRE" : "OCUPADO");
        if (p.estaDisponible()) { libres++; }
    }
    System.out.printf("Disponibles: %d de %d%n", libres, recursos.length);
}
Prestable[] inventario = {
    new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
    new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
    new SalaReuniones("SALA-A", 12)
};
informarDisponibilidad(inventario);
  Libro        plazo 15 dias  LIBRE
  Dvd          plazo  3 dias  LIBRE
  SalaReuniones plazo  1 dias  LIBRE
Disponibles: 3 de 3

Libro y SalaReuniones no comparten superclase (más allá de Object), no comparten campos, no comparten nada. Y aun así viajan juntos en el mismo array y responden a la misma llamada. Eso es lo que la herencia no podía darte.

Se sigue usando un array porque las colecciones (ArrayList y compañía) llegan en el módulo 5. Es provisional, como viene siéndolo desde el módulo 3.

  1. Los miembros de una interfaz y sus modificadores implícitos

Las interfaces aplican modificadores por defecto que no se escriben. Conocerlos evita sorpresas.

Miembro Modificadores implícitos ¿Se pueden omitir? Desde
Método sin cuerpo public abstract Sí, y debes omitirlos Java 1.0
Campo public static final Sí Java 1.0
Método default public Sí (default es obligatorio) Java 8
Método static public Sí (static es obligatorio) Java 8
Método private ninguno (private explícito) No Java 9
Tipo anidado (clase, interfaz, enum) public static Sí Java 1.1

Dos consecuencias que sorprenden a todo el mundo:

Todo campo de una interfaz es una constante. Esto:

public interface Prestable {
    int PLAZO_MAXIMO_DIAS = 30;     // realmente: public static final int
}

no declara un campo de instancia, sino una constante compartida a la que se accede como Prestable.PLAZO_MAXIMO_DIAS. No puedes asignarle otro valor, ni en el constructor ni en ningún sitio. Si intentas PLAZO_MAXIMO_DIAS = 40; obtienes cannot assign a value to final variable.

No existen los miembros de instancia. Una interfaz jamás guarda datos por objeto. Si tu diseño necesita que el contrato lleve estado asociado, el contrato no es lo que buscas: necesitas una clase abstracta (04-02).

Antipatrón: la "interfaz de constantes". Antes de los enum era habitual crear una interfaz solo para agrupar constantes e implementarla para "heredarlas" sin cualificar. Es una mala práctica reconocida: contamina la API pública de la clase con detalles de implementación. Para constantes usa una clase final con miembros static final, o mejor un enum (04-07).

  1. Métodos default: evolucionar una API sin romper nada

Hasta Java 7, añadir un método a una interfaz publicada era una catástrofe: cada una de las clases que la implementaban en el mundo dejaba de compilar de golpe. Piensa en la magnitud del problema: cuando el equipo de Java quiso añadir forEach y stream a java.util.Collection en 2014, había millones de clases implementando List fuera de su control.

La solución fueron los métodos default: métodos de interfaz con cuerpo, que las clases implementadoras heredan gratis y pueden sobrescribir si quieren.

public interface Prestable {

    boolean prestar();
    boolean devolver();
    boolean estaDisponible();
    int     getDiasPrestamo();

    /**
     * Dias que quedan de plazo. Implementacion por defecto suficiente
     * para la mayoria de recursos; los que tengan reglas propias la sobrescriben.
     */
    default int diasRestantes(int diasTranscurridos) {
        return Math.max(0, getDiasPrestamo() - diasTranscurridos);
    }

    /** @return true si el plazo se ha superado. */
    default boolean estaVencido(int diasTranscurridos) {
        return diasRestantes(diasTranscurridos) == 0 && diasTranscurridos > 0;
    }
}

Añadir estos dos métodos no rompe SalaReuniones ni Material: las dos siguen compilando sin tocar una línea y ganan los dos métodos nuevos.

Cuatro claves sobre los default:

  • Un default solo puede usar el propio contrato. Fíjate en que diasRestantes llama a getDiasPrestamo(), un método abstracto de la interfaz. No puede acceder a campos, porque no hay campos.
  • Se pueden sobrescribir. Una clase que necesite otra fórmula escribe su propio diasRestantes con @Override, y su versión gana por despacho dinámico.
  • No son un sustituto de la clase abstracta. Son un mecanismo de evolución de APIs, no una vía para meter lógica de negocio en las interfaces. Si te encuentras escribiendo default largos y con mucha lógica, revisa el diseño.
  • Object gana siempre. No puedes declarar un default para toString, equals o hashCode: el compilador lo rechaza. Las implementaciones de Object tienen prioridad estructural sobre cualquier default.

  1. Conflicto de default y Interfaz.super.metodo()

Con los default, el diamante vuelve en versión reducida. Si una clase implementa dos interfaces que aportan el mismo default, hay ambigüedad real:

public interface Prestable {
    default String describirPlazo() { return "Plazo estandar de prestamo"; }
}

public interface Reservable {
    default String describirPlazo() { return "Plazo de reserva por franjas"; }
}

public class SalaReuniones implements Prestable, Reservable { }   // NO COMPILA
error: class SalaReuniones inherits unrelated defaults for describirPlazo()
       from types Prestable and Reservable

Java no adivina: te obliga a decidir. La clase debe sobrescribir el método, y dentro puede invocar explícitamente la versión de una interfaz concreta con la sintaxis NombreInterfaz.super.metodo():

public class SalaReuniones implements Prestable, Reservable {

    @Override
    public String describirPlazo() {
        // Opcion A: quedarse con una
        return Reservable.super.describirPlazo();

        // Opcion B: combinarlas
        // return Prestable.super.describirPlazo() + " / " + Reservable.super.describirPlazo();

        // Opcion C: escribir algo completamente propio
        // return "Sala reservable por medias jornadas";
    }
}

Las reglas de resolución que aplica el compilador, en orden:

  1. La clase gana a la interfaz. Un método heredado de una superclase tiene prioridad sobre cualquier default (class wins).
  2. La interfaz más específica gana. Si Reservable extends Prestable y ambas definen el default, gana Reservable.
  3. Si no hay ganador, error de compilación, y la clase debe resolver con Interfaz.super.metodo().

La diferencia con el diamante clásico es decisiva: el conflicto se detecta en compilación y solo afecta al comportamiento, nunca al estado. Nunca hay dos copias de un campo.

  1. Métodos static en interfaces

Java 8 también permitió métodos static con cuerpo dentro de una interfaz. Son utilidades ligadas al contrato, que se invocan por el nombre de la interfaz y no se heredan por las clases implementadoras.

public interface Prestable {

    int PLAZO_MAXIMO_DIAS = 30;

    boolean prestar();
    boolean devolver();
    boolean estaDisponible();
    int     getDiasPrestamo();

    /** Comprueba que un plazo propuesto respeta la politica de la empresa. */
    static boolean plazoValido(int dias) {
        return dias > 0 && dias <= PLAZO_MAXIMO_DIAS;
    }

    /** Cuenta cuantos recursos del array estan libres. */
    static int contarDisponibles(Prestable[] recursos) {
        int total = 0;
        for (Prestable p : recursos) {
            if (p.estaDisponible()) { total++; }
        }
        return total;
    }
}
System.out.println(Prestable.plazoValido(15));                 // true
System.out.println(Prestable.plazoValido(45));                 // false
System.out.println(Prestable.contarDisponibles(inventario));   // 3

// SalaReuniones.plazoValido(15);   // NO COMPILA: los static de interfaz no se heredan

Su utilidad: mantener juntas la interfaz y sus utilidades, en lugar de crear una clase auxiliar aparte. Antes de Java 8 esto no era posible, y por eso el JDK está lleno de parejas como Collection/Collections o Path/Paths. Con métodos estáticos en interfaces, ese desdoblamiento ya no hace falta: por eso las APIs modernas ofrecen directamente List.of(...), Comparator.comparing(...) o Path.of(...).

  1. Métodos private en interfaces (Java 9)

Con varios default en una interfaz aparece pronto código duplicado entre ellos. Sacarlo a un método public lo convertiría en parte del contrato, que no es lo que quieres. Java 9 permitió métodos private en interfaces exactamente para eso:

public interface Notificable {

    String getCanalAviso();
    String generarAviso(int diasTranscurridos);

    default String avisoUrgente(int diasTranscurridos) {
        return cabecera("URGENTE") + generarAviso(diasTranscurridos);
    }

    default String avisoRutinario(int diasTranscurridos) {
        return cabecera("INFO") + generarAviso(diasTranscurridos);
    }

    /** Detalle de implementacion compartido: NO forma parte del contrato. */
    private String cabecera(String nivel) {
        return "[" + nivel + " / " + getCanalAviso() + "] ";
    }
}

Hay dos variantes:

  • private: puede llamarse desde los métodos default (tiene acceso a this).
  • private static: puede llamarse desde los default y desde los static, pero no accede a this.

Ninguna es visible desde fuera ni desde las clases implementadoras. Son puro detalle interno.

  1. Interfaces marcadoras y @FunctionalInterface

Una interfaz marcadora (marker interface) es una interfaz sin ningún método. No promete comportamiento: promete una propiedad, que el compilador o la biblioteca comprueban en tiempo de ejecución.

public interface Auditable { }   // sin metodos: solo marca

Las tres del JDK que verás:

Interfaz marcadora Qué marca Dónde se estudia
java.io.Serializable El objeto puede convertirse en bytes y guardarse Módulo 7
java.lang.Cloneable Object.clone() puede copiarlo (desaconsejado, 03-09) —
java.util.RandomAccess La lista permite acceso indexado eficiente Módulo 5

Hoy las anotaciones (@Deprecated, @Override, y las tuyas propias en 10-02) cubren buena parte de estos casos con más flexibilidad, pero las marcadoras siguen ahí porque tienen una ventaja que las anotaciones no tienen: crean un tipo, y por tanto el compilador puede exigirlas en una firma (void guardar(Serializable objeto)).

Caso aparte es @FunctionalInterface: no es una interfaz marcadora, sino una anotación que se aplica a interfaces con exactamente un método abstracto y que habilita el uso de lambdas. Se menciona aquí para que reconozcas la palabra; su significado completo, el catálogo de java.util.function y las referencias a métodos son el contenido de la lección 04-06.

  1. Diseñar con interfaces: contrato, inversión de dependencias y testabilidad

Las interfaces no son solo un truco para esquivar la herencia múltiple. Son la herramienta central de diseño de Java, y estas tres ideas explican por qué.

Programar contra el contrato, no contra la implementación. Declara siempre con el tipo más general que te sirva:

Prestable recurso = new SalaReuniones("SALA-A", 12);   // bien: dependes del contrato
SalaReuniones sala = new SalaReuniones("SALA-A", 12);  // solo si necesitas getCapacidad()

Con la primera forma, cambiar mañana a otra implementación cuesta una línea. Con la segunda, cuesta revisar todo lo que usa la variable.

Inversión de dependencias, en una frase. Los módulos importantes (las reglas de negocio) no deben depender de los módulos de detalle (la base de datos, la consola, la red): ambos deben depender de una interfaz definida por el módulo importante. En BiblioTech, GestorPrestamos no debería depender de ReciboConsola, sino de una interfaz Recibo que ReciboConsola implementa; así, cambiar a ReciboPdf no toca el gestor. Ese es el mecanismo sobre el que se apoyan Spring y la inyección de dependencias del módulo 11.

Testabilidad. Si GestorPrestamos depende de la interfaz Recibo, en una prueba puedes pasarle una implementación falsa que solo cuenta cuántas veces la han llamado, sin imprimir nada. Sin interfaz, probar el gestor obliga a capturar System.out. Esta técnica —los mocks— es JUnit y Mockito en el módulo 11, y su viabilidad se decide aquí, en el momento en que eliges depender de un contrato o de una clase concreta.

  1. Interfaz frente a clase abstracta: el avance

La pregunta llega inevitablemente: si un default puede llevar cuerpo, ¿en qué se diferencia una interfaz de una clase abstracta? En lo esencial:

  • Una interfaz no tiene estado de instancia ni constructor, y se pueden implementar varias.
  • Una clase abstracta sí tiene campos, constructor y miembros protected, pero solo puedes extender una.

Regla de partida: la interfaz define qué se puede hacer; la clase abstracta comparte cómo se hace y qué datos hacen falta. La tabla comparativa completa, con las seis dimensiones que importan y una regla práctica de decisión, es el contenido de la lección 04-02, donde además verás que lo habitual no es elegir, sino combinarlas.

  1. BiblioTech: Prestable, Notificable y SalaReuniones

Apliquemos todo al proyecto. Material tiene hoy dos grupos de responsabilidades mezclados: prestarse y avisar. Vamos a extraerlos como dos contratos independientes.

Prestable

package com.nexussoftware.bibliotech.dominio;

/** Contrato de todo recurso que Nexus Software presta a sus empleados. */
public interface Prestable {

    /** Plazo maximo que admite la politica de la empresa. */
    int PLAZO_MAXIMO_DIAS = 30;

    boolean prestar();
    boolean devolver();
    boolean estaDisponible();
    int     getDiasPrestamo();

    /** Dias de plazo que quedan; 0 si ya ha vencido. */
    default int diasRestantes(int diasTranscurridos) {
        return Math.max(0, getDiasPrestamo() - diasTranscurridos);
    }

    /** @return true si el plazo se ha superado. */
    default boolean estaVencido(int diasTranscurridos) {
        return diasTranscurridos > getDiasPrestamo();
    }

    static boolean plazoValido(int dias) {
        return dias > 0 && dias <= PLAZO_MAXIMO_DIAS;
    }

    static int contarDisponibles(Prestable[] recursos) {
        int total = 0;
        for (Prestable p : recursos) {
            if (p.estaDisponible()) { total++; }
        }
        return total;
    }
}

Notificable

package com.nexussoftware.bibliotech.dominio;

/** Contrato de todo aquello sobre lo que BiblioTech puede emitir avisos. */
public interface Notificable {

    /** Canal por el que se avisa: "correo", "chat", "telefono"... */
    String getCanalAviso();

    /** Texto del aviso para los dias transcurridos indicados. */
    String generarAviso(int diasTranscurridos);

    /** Aviso con prefijo de nivel, construido sobre el contrato. */
    default String avisoUrgente(int diasTranscurridos) {
        return cabecera("URGENTE") + generarAviso(diasTranscurridos);
    }

    default String avisoRutinario(int diasTranscurridos) {
        return cabecera("INFO") + generarAviso(diasTranscurridos);
    }

    private String cabecera(String nivel) {
        return "[" + nivel + " / " + getCanalAviso() + "] ";
    }
}

Material firma los dos contratos

El cambio en Material es de una sola línea, porque los métodos ya existen desde el módulo 3:

public class Material implements Prestable, Notificable {

    // ... constantes, campos y constructor sin cambios ...

    @Override public boolean prestar()          { /* sin cambios */ }
    @Override public boolean devolver()         { /* sin cambios */ }
    @Override public boolean estaDisponible()   { return disponible; }
    @Override public int     getDiasPrestamo()  { return DIAS_PRESTAMO; }

    @Override public String  getCanalAviso()    { return "correo"; }
    @Override public String  generarAviso(int diasTranscurridos) { /* sin cambios */ }

    // ... el resto igual ...
}

Que la refactorización cueste una línea es la mejor señal posible: significa que en el módulo 3 identificaste bien las responsabilidades. Lo único nuevo es que ahora esas responsabilidades tienen nombre y son un tipo.

SalaReuniones: la ventaja frente a la herencia

package com.nexussoftware.bibliotech.dominio;

/** Sala de reunion de Nexus Software. Se reserva, pero NO es un material. */
public class SalaReuniones implements Prestable {

    private final String codigo;
    private final int    capacidad;
    private boolean      libre;
    private String       ocupadaPor;

    public SalaReuniones(String codigo, int capacidad) {
        this.codigo     = (codigo == null || codigo.isBlank()) ? "SALA-000" : codigo.trim();
        this.capacidad  = Math.max(1, capacidad);
        this.libre      = true;
        this.ocupadaPor = null;
    }

    @Override
    public boolean prestar() {
        if (!libre) {
            System.out.println("AVISO: la sala " + codigo + " ya esta reservada.");
            return false;
        }
        libre = false;
        return true;
    }

    @Override
    public boolean devolver() {
        if (libre) { return false; }
        libre      = true;
        ocupadaPor = null;
        return true;
    }

    @Override public boolean estaDisponible()  { return libre; }
    @Override public int     getDiasPrestamo() { return 1; }

    /** Una sala no tiene multa: sobrescribe el default porque su regla es otra. */
    @Override
    public boolean estaVencido(int diasTranscurridos) {
        return false;
    }

    public String getCodigo()    { return codigo; }
    public int    getCapacidad() { return capacidad; }
}

Fíjate en lo que se ha conseguido:

  • SalaReuniones no hereda de nada. No tiene título, ni referencia, ni multa, ni tarifa. Su única deuda es el contrato Prestable.
  • No implementa Notificable, porque no se avisa de salas. Los contratos son independientes: se firman por separado.
  • Sobrescribe un default (estaVencido) porque su regla de negocio difiere.
  • Y aun así, viaja en el mismo array que un Libro y responde a informarDisponibilidad.

Con herencia esto era imposible sin mentir: o SalaReuniones extends Material (falso: no es un material y arrastraría ISBN y multas), o duplicabas el código de reserva.

classDiagram
    class Prestable {
        <<interface>>
        +PLAZO_MAXIMO_DIAS int
        +prestar() boolean
        +devolver() boolean
        +estaDisponible() boolean
        +getDiasPrestamo() int
        +diasRestantes(int) int
        +estaVencido(int) boolean
    }
    class Notificable {
        <<interface>>
        +getCanalAviso() String
        +generarAviso(int) String
        +avisoUrgente(int) String
        +avisoRutinario(int) String
    }
    class Material {
        -titulo String
        -referencia String
        -disponible boolean
        +calcularMulta(int) double
        +describir() String
    }
    class Libro
    class Revista
    class Dvd
    class SalaReuniones {
        -codigo String
        -capacidad int
        +getCapacidad() int
    }
    Prestable <|.. Material
    Notificable <|.. Material
    Prestable <|.. SalaReuniones
    Material <|-- Libro
    Material <|-- Revista
    Material <|-- Dvd

En el diagrama, la línea discontinua es implements y la continua es extends. Se lee de un vistazo: dos contratos, dos familias que los firman en distinta medida, y ningún parentesco forzado.

Errores Comunes y Consejos

Creer que una interfaz puede tener campos de instancia. int contador; dentro de una interfaz no es un campo mutable: es public static final int contador, y sin inicializador ni siquiera compila. Si necesitas estado compartido entre implementaciones, necesitas una clase abstracta (04-02).

Reducir la visibilidad al implementar. Si escribes boolean prestar() sin public en la clase, el compilador falla: los métodos de interfaz son public y no se pueden restringir. Es el error más frecuente en la primera implementación.

Olvidar @Override. Sin la anotación, escribir estaDisponibles() no da error inmediato: crea un método nuevo y el compilador se queja mucho más tarde, con un mensaje sobre métodos abstractos sin implementar. @Override señala el punto exacto del fallo.

Usar la interfaz como bolsa de constantes. Es un antipatrón conocido. Para constantes agrupadas usa un enum (04-07) o una clase final con miembros static final.

Llenar la interfaz de métodos default. Los default existen para evolucionar APIs, no para colar lógica de negocio en el contrato. Un default de treinta líneas es casi siempre una clase abstracta mal ubicada.

Interfaces demasiado grandes. Si Prestable acumulara quince métodos, ninguna clase podría implementarla sin métodos vacíos. Es preferible varias interfaces pequeñas y cohesivas —Prestable, Notificable, Reservable— que una gigante. El principio se llama segregación de interfaces y lo formalizarás en 12-02.

Consejo: nombra las interfaces por la capacidad. El convenio de Java favorece adjetivos o participios (Prestable, Notificable, Comparable, Runnable, Serializable) frente a sustantivos, precisamente porque describen lo que algo sabe hacer. Y evita los prefijos tipo IPrestable: no son convención en Java.

Consejo: declara con el tipo del contrato. Prestable recurso = ... en lugar de SalaReuniones sala = ... siempre que no necesites los métodos propios de la clase. Es la práctica que hace posible cambiar la implementación mañana.

Ejercicios

Ejercicio 1: la interfaz Catalogable

Crea una interfaz Catalogable en com.nexussoftware.bibliotech.dominio con:

  • Una constante String PREFIJO_FICHA = "FICHA-".
  • Dos métodos abstractos: String getReferencia() y String getTitulo().
  • Un método default String generarFicha() que devuelva FICHA-<referencia>: <titulo>.
  • Un método static boolean referenciaValida(String ref) que devuelva true si la referencia no es nula y tiene al menos 5 caracteres.

Después haz que Material la implemente (comprueba que no hay que escribir ni un método nuevo) y prueba generarFicha() con "Java Efectivo".

Ejercicio 2: resolver un conflicto de default

Crea dos interfaces, Prestable y Alquilable, ambas con un default String politica() que devuelva textos distintos ("Prestamo gratuito para empleados" y "Alquiler con tarifa diaria"). Crea una clase ProyectorPortatil que implemente las dos. Comprueba que no compila, y resuélvelo con Interfaz.super.metodo() devolviendo la concatenación de ambas políticas.

Ejercicio 3: un inventario heterogéneo

Escribe un método static void resumenInventario(Prestable[] recursos) que recorra el array y muestre, para cada recurso: su nombre de clase, si está disponible, y su plazo. Al final debe imprimir cuántos son Notificable (usando instanceof) y el total de disponibles con Prestable.contarDisponibles. Pruébalo con dos libros, un DVD y dos salas.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.dominio;

/** Contrato de todo elemento que aparece en el catalogo de BiblioTech. */
public interface Catalogable {

    // public static final implicito
    String PREFIJO_FICHA = "FICHA-";

    // public abstract implicitos
    String getReferencia();
    String getTitulo();

    /**
     * Ficha de catalogo. Solo usa el propio contrato: por eso puede
     * tener cuerpo sin acceder a ningun campo.
     */
    default String generarFicha() {
        return PREFIJO_FICHA + getReferencia() + ": " + getTitulo();
    }

    /** Utilidad asociada al contrato; NO se hereda por las clases. */
    static boolean referenciaValida(String ref) {
        return ref != null && ref.trim().length() >= 5;
    }
}

Material solo cambia en la declaración:

public class Material implements Prestable, Notificable, Catalogable {
    // getReferencia() y getTitulo() YA EXISTEN desde 03-07: nada que anadir
}
Catalogable c = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(c.generarFicha());
System.out.println(Catalogable.referenciaValida("978-0000000001"));
System.out.println(Catalogable.referenciaValida("123"));
FICHA-978-0000000001: Java Efectivo
true
false

Lo importante del ejercicio: implementar la interfaz no ha costado ni una línea de cuerpo. Cuando los métodos ya existen con la firma correcta, firmar el contrato es declarativo. Y generarFicha() llega gratis a Libro, Revista y Dvd a la vez.

Solución 2

public interface Prestable2 {
    default String politica() { return "Prestamo gratuito para empleados"; }
}

public interface Alquilable {
    default String politica() { return "Alquiler con tarifa diaria"; }
}

Sin sobrescribir, el error es:

error: class ProyectorPortatil inherits unrelated defaults for politica()
       from types Prestable2 and Alquilable

La resolución:

public class ProyectorPortatil implements Prestable2, Alquilable {

    private final String codigo;

    public ProyectorPortatil(String codigo) { this.codigo = codigo; }

    /**
     * El compilador obliga a decidir. Aqui se combinan ambas politicas:
     * el proyector es gratuito para empleados, pero se alquila a externos.
     */
    @Override
    public String politica() {
        return Prestable2.super.politica() + "; " + Alquilable.super.politica();
    }

    public String getCodigo() { return codigo; }
}
System.out.println(new ProyectorPortatil("PRO-01").politica());
Prestamo gratuito para empleados; Alquiler con tarifa diaria

Nota la sintaxis Prestable2.super.politica(): es la única forma de invocar explícitamente el default de una interfaz concreta, y solo es válida si la clase implementa directamente esa interfaz. Es el mecanismo con el que Java conserva la implementación múltiple sin el problema del diamante: el conflicto no se resuelve por reglas ocultas, lo resuelve el programador, y solo afecta al comportamiento, nunca al estado.

Solución 3

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.dominio.*;

public class InventarioApp {

    /** Resumen valido para cualquier recurso prestable, sea del tipo que sea. */
    public static void resumenInventario(Prestable[] recursos) {
        System.out.println("=== INVENTARIO DE RECURSOS PRESTABLES ===");

        int notificables = 0;

        for (Prestable p : recursos) {
            System.out.printf("  %-15s %-10s plazo %2d dias%n",
                              p.getClass().getSimpleName(),
                              p.estaDisponible() ? "LIBRE" : "OCUPADO",
                              p.getDiasPrestamo());

            // instanceof con patron de tipo (03-06): comprueba y convierte
            if (p instanceof Notificable n) {
                notificables++;
                System.out.printf("      canal de aviso: %s%n", n.getCanalAviso());
            }
        }

        System.out.printf("Recursos notificables: %d de %d%n", notificables, recursos.length);
        System.out.printf("Disponibles ahora:     %d de %d%n",
                          Prestable.contarDisponibles(recursos), recursos.length);
    }

    public static void main(String[] args) {

        Prestable[] inventario = {
            new Libro("Java Efectivo",      "Joshua Bloch",  "978-0000000001", 2018),
            new Libro("Patrones de Diseno", "Erich Gamma",   "978-0000000002", 1994),
            new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
            new SalaReuniones("SALA-A", 12),
            new SalaReuniones("SALA-B", 4)
        };

        inventario[0].prestar();          // Marta Ruiz se lleva Java Efectivo
        inventario[3].prestar();          // y reserva la SALA-A

        resumenInventario(inventario);
    }
}
=== INVENTARIO DE RECURSOS PRESTABLES ===
  Libro           OCUPADO    plazo 15 dias
      canal de aviso: correo
  Libro           LIBRE      plazo 15 dias
      canal de aviso: correo
  Dvd             LIBRE      plazo  3 dias
      canal de aviso: telefono
  SalaReuniones   OCUPADO    plazo  1 dias
  SalaReuniones   LIBRE      plazo  1 dias
Recursos notificables: 3 de 5
Disponibles ahora:     3 de 5

Tres observaciones sobre la solución:

  1. El método no menciona ni una sola clase concreta. Solo conoce Prestable y Notificable. Añadir mañana EquipoInformatico implements Prestable no obliga a tocar ni una línea de resumenInventario.
  2. instanceof con patrón distingue a los que además firman Notificable, y en la misma línea declara la variable n ya convertida. No es un olor de diseño como los switch sobre el tipo de 03-06: aquí no se elige comportamiento por tipo, se comprueba una capacidad opcional.
  3. Prestable.contarDisponibles se invoca sobre la interfaz, no sobre un objeto. Es un método static de interfaz: utilidad y contrato viven en el mismo fichero.

Conclusión

Ya tienes en la mano la herramienta central del diseño en Java. Sabes que una interfaz es un contrato de comportamiento sin estado, que se declara con interface y se firma con implements, y que expresa una relación de "puede hacer" frente al "es un" de la herencia. Entiendes por qué Java permite implementar tantas interfaces como quieras pero solo extender una clase: el problema del diamante, con su ambigüedad de estado y de implementación, desaparece cuando el contrato no aporta ni campos ni cuerpos. Sabes que una interfaz es un tipo, y has visto la consecuencia práctica en un array donde un Libro y una SalaReuniones —sin ningún parentesco— responden a la misma llamada.

Dominas los miembros de una interfaz y sus modificadores implícitos: public abstract en los métodos, public static final en los campos, sin excepción. Conoces los métodos default y, sobre todo, para qué se añadieron: permitir que el JDK evolucionara sus interfaces sin romper millones de clases; y sabes resolver su único conflicto posible con Interfaz.super.metodo(). Sabes que los métodos static mantienen las utilidades junto al contrato —por eso hoy existe List.of(...) donde antes hacía falta la clase Collections— y que los private de Java 9 permiten compartir código entre default sin ampliar el contrato. Reconoces las interfaces marcadoras como Serializable (módulo 7) y sabes que @FunctionalInterface es la puerta a las lambdas, que abrirás en 04-06.

Y por encima de la sintaxis, te llevas tres criterios de diseño: programa contra el contrato, invierte las dependencias para que las reglas de negocio no dependan de los detalles, y recuerda que la testabilidad de tu código en el módulo 11 se decide hoy, cada vez que eliges depender de una interfaz o de una clase concreta. BiblioTech ya tiene sus contratos, Prestable y Notificable, y una SalaReuniones que demuestra que se puede prestar sin ser un material.

Queda una grieta evidente. Material sigue siendo una clase instanciable: nada impide escribir new Material("Algo", "REF-1", true) y obtener un objeto sin tipo, sin autor, sin número y sin duración, que devuelve "Material" en getTipo() y aplica plazos genéricos. Es un objeto que no debería existir, y su getTipo() y su getDiasPrestamo() no son más que valores de relleno que las subclases sobrescriben siempre. En la lección 04-02, Clases Abstractas, cerrarás esa puerta: Material pasará a ser abstract, sus métodos de relleno se convertirán en métodos abstractos que obligan a cada soporte a declarar sus reglas, y descubrirás que una clase abstracta ofrece lo que una interfaz no puede —estado, constructor y código compartido— y por qué el diseño profesional casi nunca elige entre ambas, sino que las combina.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados