W3docs

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:

  1. Métodos default — ya tienen cuerpo, por lo que quien la implemente no necesita proporcionar uno.
  2. Métodos static — pertenecen a la propia interfaz, no a quienes la implementan.
  3. Métodos abstractos public que sobreescriben un método de java.lang.Object — p. ej. equals, hashCode, toString. Cada clase ya hereda implementaciones de Object, 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:

  1. 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.
  2. 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 que Function<T, ValidationResult> aunque la estructura coincida.
  • Necesitas una excepción comprobada. Function.apply no lanza nada comprobado; si tu operación lanza IOException, 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't

Una 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.

java— editable, runs on the server

Lo que extraer de la ejecución:

  • notBlank1 (lambda), notBlank2 (cadena de referencia a método) y notBlank3 (clase anónima) implementan la misma interfaz Filter<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étodos default and y negate en la interfaz son el álgebra de composición — por eso el JDK los añade a Predicate, Function y Comparator también.
  • SafelyFunctional<T> declara tanto apply(T) como boolean equals(Object), y aun así compiló con @FunctionalInterface. La sobreescritura de equals se hereda de Object, por lo que no cuenta contra la regla del método abstracto único.
  • Si eliminas la palabra clave default en Filter (convirtiendo un método default en un segundo método abstracto), la anotación @FunctionalInterface fuerza 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.functionFunction, Predicate, Consumer, Supplier, sus variantes bi, y las especializaciones primitivas que existen para evitar el boxing.

Práctica

Práctica
Una interfaz declara tres métodos: un método abstracto, un método `default`, y `boolean equals(Object)` redeclarado de `Object`. ¿Es una `@FunctionalInterface` válida?
Una interfaz declara tres métodos: un método abstracto, un método `default`, y `boolean equals(Object)` redeclarado de `Object`. ¿Es una `@FunctionalInterface` válida?
Was this page helpful?