Java
Interfaces: contratos entre clases
Qué es una interfaz, en qué se diferencia de una clase abstracta, por qué una clase puede implementar varias, y cómo se usan las interfaces de la biblioteca estándar.
El problema: cosas que comparten una capacidad, no un origen
Una Factura, una Cancion y un Alumno no tienen nada que ver entre sí: no comparten atributos, no tienen un padre común razonable, y forzar un extends entre ellas sería inventar un parentesco falso. Y sin embargo las tres pueden necesitar la misma capacidad: por ejemplo, saber exportarse a texto para guardarlas en un archivo.
Con herencia no se puede resolver: Java permite extends una sola vez, y en cualquier caso una Cancion no es un Alumno. Lo que hace falta no es un padre, sino un contrato: una lista de métodos que quien lo firme se compromete a tener. Eso es una interfaz.
Declarar e implementar una interfaz
interface se traduce como “interfaz”: la superficie por la que dos cosas se comunican, igual que la interfaz de un programa es por donde lo usas sin ver su interior.
public interface Exportable {
String aTexto();
}Eso es todo: la palabra interface, y adentro las firmas de los métodos terminadas en ;, sin cuerpo. Una clase se compromete a cumplirlo con implements (“implementa”):
public class Factura implements Exportable {
private int numero;
private double total;
public Factura(int numero, double total) {
this.numero = numero;
this.total = total;
}
@Override
public String aTexto() {
return "FACTURA;" + numero + ";" + total;
}
}
public class Cancion implements Exportable {
private String titulo;
private int segundos;
public Cancion(String titulo, int segundos) {
this.titulo = titulo;
this.segundos = segundos;
}
@Override
public String aTexto() {
return "CANCION;" + titulo + ";" + segundos;
}
}Si una clase escribe implements Exportable y no define aTexto(), no compila: Factura is not abstract and does not override abstract method aTexto(). El contrato se hace cumplir en el compilador, no en la buena voluntad de quien escribe.
Una interfaz también es un tipo
Y aquí está la ganancia, la misma del polimorfismo pero sin necesitar un padre común:
public static void guardarTodo(ArrayList<Exportable> items) {
for (Exportable item : items) {
System.out.println(item.aTexto());
}
}ArrayList<Exportable> items = new ArrayList<>();
items.add(new Factura(1001, 25990.5));
items.add(new Cancion("Bohemian Rhapsody", 355));
guardarTodo(items);FACTURA;1001;25990.5
CANCION;Bohemian Rhapsody;355guardarTodo no sabe nada de facturas ni de canciones: solo sabe que lo que reciba tiene un aTexto(). Cualquier clase que se escriba mañana funcionará con este método sin tocarle una línea.
Varias interfaces a la vez
Esta es la diferencia práctica más grande con la herencia: extends va una sola vez, implements acepta una lista separada por comas.
public interface Exportable {
String aTexto();
}
public interface Comparable2 {
int compararCon(Object otro);
}
public class Factura implements Exportable, Comparable2 {
// debe definir aTexto() Y compararCon()
}Y las dos cosas se combinan: una clase puede heredar de una y firmar varias interfaces, en ese orden.
public class Alumno extends Persona implements Exportable {
// hereda de Persona, además cumple el contrato Exportable
}Interfaz o clase abstracta: cuál usar
Las dos definen métodos que las hijas deben implementar, y al principio se confunden. La tabla:
Clase abstract | interface | |
|---|---|---|
| Palabra en la hija | extends | implements |
| ¿Cuántas se pueden tener? | Una sola | Todas las que quieras |
| ¿Puede tener atributos con estado? | Sí, normales | No (solo constantes public static final) |
| ¿Puede tener constructor? | Sí | No |
| ¿Métodos con cuerpo? | Sí, los que quiera | Sí, pero marcados default o static |
| La relación que expresa | “es un” (un Circulo es una Figura) | “es capaz de” (una Factura es exportable) |
La pregunta que decide, casi siempre:
- ¿Las clases comparten estado y son variantes de una misma cosa? → clase abstracta.
CirculoyRectanguloson los dos figuras y comparten unnombre. - ¿Las clases no tienen nada en común salvo que saben hacer algo? → interfaz.
FacturayCancionsolo comparten que saben exportarse.
La convención de nombres lo refleja: las clases abstractas suelen ser sustantivos (Figura, Vehiculo, Persona) y las interfaces suelen ser adjetivos terminados en -able o -ible (Exportable, Comparable, Runnable, Serializable). Si el nombre que se te ocurre termina en -able, casi seguro querías una interfaz.
Métodos default: cuerpo dentro de una interfaz
Desde Java 8, una interfaz puede traer implementaciones ya escritas marcadas con default:
public interface Exportable {
String aTexto();
default String aTextoConEtiqueta(String etiqueta) {
return "[" + etiqueta + "] " + aTexto();
}
}Toda clase que implemente Exportable recibe aTextoConEtiqueta gratis, y puede sobrescribirlo si le conviene. Se agregó al lenguaje para poder ampliar interfaces ya publicadas sin romper el código existente: sin default, añadir un método a una interfaz haría dejar de compilar a todas las clases del mundo que la implementan.
Que existan métodos default no convierte a las interfaces en clases abstractas. Siguen sin poder tener atributos con estado ni constructor, y ahí está la línea que las separa. Úsalos para completar el contrato, no para meter lógica de negocio.
Interfaces que ya usas sin saberlo
Buena parte de la biblioteca estándar está construida sobre interfaces. Dos que aparecen enseguida:
Comparable<T> — la que hace que ordenar funcione. Su único método es compareTo, que devuelve un número negativo, cero o positivo según el objeto vaya antes, igual o después que el otro:
public class Alumno implements Comparable<Alumno> {
private String nombre;
private double nota;
public Alumno(String nombre, double nota) {
this.nombre = nombre;
this.nota = nota;
}
@Override
public int compareTo(Alumno otro) {
return Double.compare(this.nota, otro.nota);
}
public String getNombre() {
return nombre;
}
}ArrayList<Alumno> curso = new ArrayList<>();
curso.add(new Alumno("Ana", 8.5));
curso.add(new Alumno("Beto", 6.0));
curso.add(new Alumno("Caro", 9.5));
Collections.sort(curso); // usa compareTo automáticamente
for (Alumno a : curso) {
System.out.println(a.getNombre());
}Beto
Ana
CaroCollections.sort fue escrito años antes que tu clase Alumno y no sabe nada de notas. Solo sabe que lo que le pasen implementa Comparable, y con eso le alcanza. Esa es la utilidad de las interfaces en una frase.
List<E> — el motivo de una convención que verás mucho:
List<String> nombres = new ArrayList<>();List es la interfaz y ArrayList una de sus implementaciones. Declarar la variable con el tipo de la interfaz deja el resto del código independiente de cuál lista se está usando: cambiar a LinkedList es tocar una línea.
Pon a prueba
Tienes tres clases que ya existen y no se parecen en nada — Usuario, Pedido y Mensaje — y necesitas que las tres sepan guardarse en disco con un método guardar(), para poder escribir un único método guardarTodo(...) que reciba una lista mezclada. ¿Qué conviene declarar?
Reproducible define la capacidad de reproducirse. Podcast la cumple. Escribe las palabras clave que faltan.
public Reproducible {
void reproducir()
}
public class Podcast Reproducible {
private String titulo;
public void reproducir() {
System.out.println("Reproduciendo " + titulo);
}
}Este código da dos errores de compilación distintos. ¿Cuáles son?
public interface Dibujable {
int grosor = 2;
void dibujar();
}
public class Linea implements Dibujable {
@Override
public void dibujar() {
grosor = 5;
System.out.println("Línea de grosor " + grosor);
}
}
// en main:
Dibujable d = new Dibujable();Error 1 — grosor = 5;. Un atributo declarado dentro de una interfaz es implícitamente public static final, aunque no lo escribas: es una constante, no un atributo de instancia. Asignarle un valor nuevo da cannot assign a value to final variable grosor. Esta es la razón técnica detrás de “las interfaces no tienen estado” de la tabla comparativa.
Error 2 — new Dibujable(). Una interfaz no se puede instanciar, por el mismo motivo que una clase abstracta: no tiene constructor ni implementación que ejecutar. Dibujable sirve perfectamente como tipo de la variable (Dibujable d = new Linea(); es correcto), pero lo que va del lado derecho del = tiene que ser una clase concreta que la implemente.
Ordena las líneas para declarar una interfaz Pagable y una clase que la implementa.
- }
- @Override
- return tarifa * 1.19;
- public interface Pagable {
- }
- }
- private double tarifa;
- public class Servicio implements Pagable {
- double montoAPagar();
- public double montoAPagar() {
Siguiente paso
Con clases, herencia, polimorfismo e interfaces ya tienes las cuatro piezas grandes de la orientación a objetos. Falta cerrar un detalle que apareció desde el primer public static void main: qué significa exactamente que algo sea static, y qué garantiza final — ver Object, toString y equals primero, y después static y final.