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
- Qué es una interfaz: un contrato sin estado
- Sintaxis e
implements - Implementación múltiple y el problema del diamante
- Una interfaz es un TIPO: polimorfismo por contrato
- Los miembros de una interfaz y sus modificadores implícitos
- Métodos
default: evolucionar una API sin romper nada - Conflicto de
defaultyInterfaz.super.metodo() - Métodos
staticen interfaces - Métodos
privateen interfaces (Java 9) - Interfaces marcadoras y
@FunctionalInterface - Diseñar con interfaces: contrato, inversión de dependencias y testabilidad
- Interfaz frente a clase abstracta: el avance
- BiblioTech:
Prestable,NotificableySalaReuniones - Errores Comunes y Consejos
- Ejercicios
- 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".
Libroes unMaterial(herencia).Libropuede 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 |
- Sintaxis e
implements
implementsUna 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:
- 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 claseabstract, y entonces la obligación pasa a sus subclases (lección 04-02). - Los métodos implementados deben ser
public. Como el método de la interfaz es implícitamentepublic, 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.
- 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:
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 camposcodigo?- Si
RecursoFisicoyRecursoDigitalheredan 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.
- 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()); // falseLa 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 (
ArrayListy compañía) llegan en el módulo 5. Es provisional, como viene siéndolo desde el módulo 3.
- 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:
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
enumera 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 miembrosstatic final, o mejor unenum(04-07).
- Métodos
default: evolucionar una API sin romper nada
default: evolucionar una API sin romper nadaHasta 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
defaultsolo puede usar el propio contrato. Fíjate en quediasRestantesllama agetDiasPrestamo(), 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
diasRestantescon@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
defaultlargos y con mucha lógica, revisa el diseño. Objectgana siempre. No puedes declarar undefaultparatoString,equalsohashCode: el compilador lo rechaza. Las implementaciones deObjecttienen prioridad estructural sobre cualquierdefault.
- Conflicto de
default y Interfaz.super.metodo()
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 COMPILAerror: class SalaReuniones inherits unrelated defaults for describirPlazo()
from types Prestable and ReservableJava 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:
- La clase gana a la interfaz. Un método heredado de una superclase tiene prioridad sobre cualquier
default(class wins). - La interfaz más específica gana. Si
Reservable extends Prestabley ambas definen eldefault, ganaReservable. - 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.
- Métodos
static en interfaces
static en interfacesJava 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 heredanSu 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(...).
- Métodos
private en interfaces (Java 9)
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étodosdefault(tiene acceso athis).private static: puede llamarse desde losdefaulty desde losstatic, pero no accede athis.
Ninguna es visible desde fuera ni desde las clases implementadoras. Son puro detalle interno.
- Interfaces marcadoras y
@FunctionalInterface
@FunctionalInterfaceUna 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.
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.
- 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.
- 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.
- BiblioTech:
Prestable, Notificable y SalaReuniones
Prestable, Notificable y SalaReunionesApliquemos 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:
SalaReunionesno hereda de nada. No tiene título, ni referencia, ni multa, ni tarifa. Su única deuda es el contratoPrestable.- 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
Libroy responde ainformarDisponibilidad.
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()yString getTitulo(). - Un método
default String generarFicha()que devuelvaFICHA-<referencia>: <titulo>. - Un método
static boolean referenciaValida(String ref)que devuelvatruesi 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"));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 AlquilableLa 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; }
}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 5Tres observaciones sobre la solución:
- El método no menciona ni una sola clase concreta. Solo conoce
PrestableyNotificable. Añadir mañanaEquipoInformatico implements Prestableno obliga a tocar ni una línea deresumenInventario. instanceofcon patrón distingue a los que además firmanNotificable, y en la misma línea declara la variablenya convertida. No es un olor de diseño como losswitchsobre el tipo de 03-06: aquí no se elige comportamiento por tipo, se comprueba una capacidad opcional.Prestable.contarDisponiblesse invoca sobre la interfaz, no sobre un objeto. Es un métodostaticde 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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
