Interfaces Funcionales en Java
Interfaces con un único método abstracto en Java que sirven como destino para lambdas, marcadas con @FunctionalInterface.
Una interfaz funcional es una interfaz con exactamente un método abstracto. Ese único método es al que se compila una lambda o una referencia a método. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — todas son interfaces funcionales. Ya existen decenas en el JDK y escribirás las tuyas propias cuando ninguna de ellas encaje.
El capítulo anterior mostró lambdas como () -> 42 y s -> s.length() que "se compilan para la interfaz que el contexto necesite." Este capítulo responde qué hace que una interfaz sea un destino válido — la regla del método abstracto único (SAM) — y cómo @FunctionalInterface te permite decir "sí, esta lo es, y quiero que el compilador lo refuerce."
La regla SAM, con precisión
Para ser funcional, una interfaz debe declarar exactamente un método que necesite una implementación. La formulación importa: no "exactamente un método en total", sino "exactamente un método abstracto". Tres categorías de métodos no cuentan en ese uno:
- Métodos
default— ya tienen cuerpo, por lo que quien la implemente no necesita proporcionar uno. - Métodos
static— pertenecen a la propia interfaz, no a quienes la implementan. - Métodos abstractos
publicque sobreescriben un método dejava.lang.Object— p. ej.equals,hashCode,toString. Cada clase ya hereda implementaciones deObject, por lo que volver a declararlos en una interfaz no añade un nuevo requisito.
El tercero sorprende a la gente. Comparator<T> declara boolean equals(Object), pero sigue siendo funcional porque ese método proviene de Object. El método abstracto real es int compare(T, T).
@FunctionalInterface
interface MyComparator<T> {
int compare(T a, T b); // the one SAM
boolean equals(Object other); // Object override — doesn't count
default MyComparator<T> reversed() { // default — doesn't count
return (a, b) -> compare(b, a);
}
static <T extends Comparable<T>> MyComparator<T> natural() { // static — doesn't count
return (a, b) -> a.compareTo(b);
}
}@FunctionalInterface — verificación en tiempo de compilación opcional
La anotación es opcional. Una interfaz es funcional según su estructura, no según si la anotas. Pero anotarla te aporta dos cosas:
- Error de compilación si la interfaz deja de ser funcional. Añade un segundo método abstracto por accidente y el compilador te detiene de inmediato — en la interfaz, no en cada sitio de llamada que la usa como destino de una lambda.
- Documentación. La anotación indica "esto está pensado para usarse como destino de una lambda", lo que vale la pena decir en cualquier caso no obvio.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
boolean isInvalid(T value); // <-- compile error: not a functional interface
}Sin la anotación, ese segundo método convertiría silenciosamente Validator<T> en una interfaz no funcional, y el primer sitio de llamada que la usara como lambda fallaría al compilar con un mensaje confuso alejado de la causa.
La anotación es también la convención de las propias interfaces funcionales del JDK — Function, Predicate, Consumer, Supplier, Runnable, Callable todas la llevan.
Lambdas, referencias a métodos y clases anónimas son intercambiables
Una interfaz funcional acepta tres tipos de valor, y son libremente intercambiables:
Predicate<String> blank1 = s -> s.trim().isEmpty(); // lambda
Predicate<String> blank2 = String::isBlank; // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() { // anonymous class
@Override public boolean test(String s) { return s.trim().isEmpty(); }
};Las tres implementan la misma interfaz Predicate<String> y producen valores equivalentes en el sitio de llamada. La lambda y la referencia a método son notablemente más cortas; la clase anónima se reserva para los casos poco frecuentes listados en el capítulo anterior (se necesita más de un método, estado local al método, this refiriéndose a la nueva instancia).
Interfaces funcionales genéricas
La interfaz puede estar parametrizada — así es como una sola declaración de Function<T, R> puede usarse para cada transformación:
@FunctionalInterface
interface Mapper<T, R> {
R map(T input);
}
Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex = n -> Integer.toHexString(n);Los parámetros pueden tener límites, pueden incluir múltiples variables de tipo y pueden reutilizarse entre interfaces — la biblioteca estándar usa cada variación.
Escribir tu propia interfaz funcional
La mayor parte del tiempo deberías usar las interfaces integradas en java.util.function — el siguiente capítulo las recorre todas. Escribe las tuyas propias cuando:
- La semántica merece un nombre.
Validator<T>se lee mejor en un sitio de llamada queFunction<T, ValidationResult>aunque la estructura coincida. - Necesitas una excepción comprobada.
Function.applyno lanza nada comprobado; si tu operación lanzaIOException, escribe un SAM que lo declare. - La estructura no está en la biblioteca estándar. Un método que toma tres argumentos (una función tri) no tiene interfaz integrada — escríbela cuando la necesites.
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T input) throws IOException;
}
IOFunction<Path, String> readAll = Files::readString; // declared exception — built-in Function can'tUna cantidad sorprendentemente grande de "¿debería escribir esto?" se reduce a legibilidad o propagación de excepciones.
Los métodos default merecen su lugar
El único lugar donde escribirás tu propia interfaz funcional y también añadirás métodos default es cuando quieras que los usuarios puedan componer instancias:
@FunctionalInterface
interface Filter<T> {
boolean keep(T value);
default Filter<T> and(Filter<T> other) {
return v -> keep(v) && other.keep(v);
}
default Filter<T> negate() {
return v -> !keep(v);
}
}
Filter<Integer> positive = n -> n > 0;
Filter<Integer> even = n -> n % 2 == 0;
Filter<Integer> posOdd = positive.and(even.negate());Esa es exactamente la receta que el JDK usa para Predicate.and / or / negate, Function.andThen / compose, y Comparator.thenComparing. El único método abstracto es el comportamiento; los métodos default son el álgebra de composición que lo rodea.
Un ejemplo práctico: escribirla, anotarla, componerla
El programa a continuación define una interfaz funcional Filter<T> con dos métodos default, demuestra la regla SAM (un método abstracto adicional no compilaría) y muestra lambdas, referencias a métodos y una clase anónima implementando el mismo SAM.
Lo que extraer de la ejecución:
notBlank1(lambda),notBlank2(cadena de referencia a método) ynotBlank3(clase anónima) implementan la misma interfazFilter<String>— de forma intercambiable. La lambda es la más corta; la clase anónima se reserva para casos que las lambdas no pueden manejar.positive.and(even.negate())compuso tres filtros en uno sin declaraciones de métodos adicionales. Los métodosdefaultandynegateen la interfaz son el álgebra de composición — por eso el JDK los añade aPredicate,FunctionyComparatortambién.SafelyFunctional<T>declara tantoapply(T)comoboolean equals(Object), y aun así compiló con@FunctionalInterface. La sobreescritura deequalsse hereda deObject, por lo que no cuenta contra la regla del método abstracto único.- Si eliminas la palabra clave
defaultenFilter(convirtiendo un método default en un segundo método abstracto), la anotación@FunctionalInterfacefuerza un error de compilación inmediato en la declaración de la interfaz — mucho antes de que cualquier sitio de llamada con lambda vea fallos de inferencia confusos.
Qué viene después
Ya puedes reconocer una interfaz funcional, escribir una cuando el JDK no tiene lo que necesitas y dejar que el compilador refuerce su estructura. Casi siempre, sin embargo, la respuesta correcta es "usa lo que ya existe." El siguiente capítulo, Interfaces Funcionales Integradas en Java, recorre java.util.function — Function, Predicate, Consumer, Supplier, sus variantes bi, y las especializaciones primitivas que existen para evitar el boxing.