Interfaces en Java
Define contratos en Java con interfaces: métodos abstractos, métodos predeterminados y herencia múltiple de tipo.
Una interfaz es un contrato: un conjunto nombrado de operaciones que cualquier clase que la implemente se compromete a proporcionar. Las interfaces no tienen estado de instancia, no tienen constructores y (con una excepción que se verá en el próximo capítulo) no tienen cuerpos de método. Describen qué puede hacer un tipo, dejando cada cómo a las implementaciones.
Las interfaces son la respuesta de Java a la herencia múltiple. Una clase extiende exactamente una clase, pero puede implementar cualquier número de interfaces, lo que permite combinar Comparable, AutoCloseable e Iterable en el mismo tipo sin ambigüedad.
Declarar una interfaz
Usa interface en lugar de class:
public interface Shape {
double area(); // implicitly public abstract
double perimeter();
}Las declaraciones de métodos en una interfaz son implícitamente public abstract. Puedes escribir esos modificadores, pero la mayoría de las guías de estilo los omiten por ser redundantes.
Implementar una interfaz
Una clase la declara con implements. La clase debe proporcionar un cuerpo para cada método que declara la interfaz:
public class Circle implements Shape {
private final double r;
public Circle(double r) { this.r = r; }
@Override public double area() { return Math.PI * r * r; }
@Override public double perimeter() { return 2 * Math.PI * r; }
}Si una clase implementa una interfaz pero no proporciona todos los métodos, la clase debe declararse abstract, igual que con las clases abstractas.
También puedes implementar muchas interfaces a la vez:
public class Money implements Comparable<Money>, java.io.Serializable {
private final long cents;
public Money(long cents) { this.cents = cents; }
public int compareTo(Money other) { return Long.compare(this.cents, other.cents); }
}Esto es "herencia múltiple de tipo": Money es a la vez un Comparable<Money> y un Serializable. El código que necesite cualquiera de los dos puede recibir un Money.
Programar contra una interfaz
El propósito de las interfaces es escribir código frente al contrato, no frente a la implementación:
public double sumAreas(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area();
return sum;
}sumAreas no sabe ni le importa Circle. Agrega Square implements Shape, Triangle implements Shape, y la función funciona con listas de ellos también, sin cambios.
La biblioteca estándar está construida sobre este patrón. Casi siempre se declara una variable de tipo interfaz y se instancia una concreta:
List<String> names = new ArrayList<>(); // List, not ArrayList
Map<String, Integer> counts = new HashMap<>(); // Map, not HashMapSi luego cambias a LinkedList o LinkedHashMap, solo cambia la línea con new.
Constantes en interfaces
Cada campo declarado en una interfaz es implícitamente public static final, es decir, una constante. Por lo general no se agregan constantes a las interfaces (se considera mala práctica, el Constant Interface Antipattern), pero la sintaxis existe:
public interface Color {
String DEFAULT = "black"; // implicitly public static final
}Si necesitas constantes, prefiere un enum o una clase final simple con campos public static final.
Las interfaces pueden extender interfaces
Una interfaz puede extender otras interfaces, incluso varias a la vez:
public interface Readable { String read(); }
public interface Writable { void write(String s); }
public interface ReadWrite extends Readable, Writable { }Ahora cualquier clase que implemente ReadWrite debe proporcionar tanto read() como write(). No hay distinción entre class extends / interface implements: las interfaces simplemente extienden interfaces con extends.
Métodos predeterminados y estáticos (vista previa)
Desde Java 8, las interfaces pueden tener métodos default (cuerpos proporcionados con la palabra clave default) y métodos static. Permiten agregar comportamiento a una interfaz sin romper todos los implementadores existentes. El tratamiento completo está en el siguiente capítulo, métodos predeterminados:
public interface Shape {
double area();
// Default method — implementors get this for free.
default String describe() {
return getClass().getSimpleName() + " area=" + area();
}
// Static factory on the interface itself.
static Shape unitCircle() { return new Circle(1); }
}Interfaces marcadoras
Una interfaz marcadora no declara métodos. Existe únicamente como una etiqueta que el código en tiempo de ejecución puede verificar:
public interface Cacheable { } // no methods
public class Snapshot implements Cacheable { ... }Luego, en otro lugar: if (obj instanceof Cacheable) { ... }. El Java moderno prefiere anotaciones para este tipo de metadatos (@Cacheable en lugar de implements Cacheable), pero Serializable, Cloneable y RandomAccess son interfaces marcadoras bien conocidas de la biblioteca estándar.
Interfaces funcionales
Una interfaz con exactamente un método abstracto es una interfaz funcional. Importa porque Java permite suministrar dicha interfaz mediante una expresión lambda o una referencia a método en lugar de una clase anónima completa: la lambda es la implementación de ese único método.
@FunctionalInterface
public interface Transformer {
String apply(String input); // the single abstract method
}
Transformer upper = s -> s.toUpperCase(); // lambda implements apply
System.out.println(upper.apply("hi")); // prints: HILa anotación opcional @FunctionalInterface es una salvaguarda en tiempo de compilación: el código no compilará si la interfaz termina teniendo más de un método abstracto. (Los métodos default y static no cuentan para el límite.) La biblioteca estándar incluye toda una caja de herramientas de estas: Runnable, Comparator, Function, Predicate, Supplier, tratadas en interfaces funcionales.
Elegir entre una interfaz y una clase abstracta
El árbol de decisión:
- ¿Las subclases necesitan compartir estado o implementaciones de métodos compartidas? Si es así, probablemente quieras una clase abstracta: las interfaces no pueden tener estado de instancia.
- ¿Quieres un tipo que varias clases sin relación entre sí puedan satisfacer? Si es así, una interfaz: una clase puede implementar muchas interfaces pero extender solo una clase.
- ¿El contrato crecerá con el tiempo? Las interfaces evolucionan con más cuidado: agregar un método abstracto a una interfaz rompe todos los implementadores, a menos que lo hagas un método
default. Las clases abstractas pueden agregar un método concreto sin romper nada.
En la mayoría de las bases de código reales, las interfaces ganan para los contratos de tipo y las clases abstractas aparecen entre bastidores cuando varias implementaciones comparten infraestructura.
Un ejemplo completo
Qué sigue
Las interfaces solían ser contratos puros, sin cuerpos de método en absoluto. Desde Java 8 eso se ha relajado: las interfaces pueden proporcionar métodos default, métodos static e incluso métodos auxiliares private. El próximo capítulo es un recorrido por esas adiciones. Continúa en métodos predeterminados.