Java Consumer y Supplier
Consumer con efectos secundarios y Supplier productor de valores: interfaces funcionales clave en Java.
Consumer<T> y Supplier<T> son las dos interfaces funcionales para los extremos no puros de la taxonomía de cuatro esquinas:
Consumer<T>recibe un valor y devuelve nada — su trabajo es el efecto secundario (imprimir, registrar, escribir, insertar en una colección).Supplier<T>no recibe nada y devuelve un valor — su trabajo es producir unTde forma perezosa, bajo demanda (valores predeterminados, fábricas, aleatoriedad).
Ambas se complementan con los capítulos de Function/Predicate anteriores: aquellas devolvían un valor a partir de un valor; estas entran y salen del mundo circundante. Este capítulo cubre ambas interfaces porque sus APIs son pequeñas y sus sitios de uso se solapan.
Consumer<T>
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // the only abstract method
default Consumer<T> andThen(Consumer<? super T> after);
}Un Consumer es "hacer algo con este T." El SAM es accept. El único método default andThen encadena consumidores para que se ejecuten en secuencia sobre la misma entrada:
Consumer<String> log = System.out::println;
Consumer<String> store = audit::record;
Consumer<String> both = log.andThen(store);
both.accept("hello"); // prints "hello", then audit.record("hello")andThen no hace cortocircuito si el primer consumidor lanza una excepción — deja que la excepción se propague y el segundo consumidor nunca se ejecuta. Tiene la misma semántica que escribir las dos llamadas en un bloque sin try: el fallo detiene la secuencia.
Dónde aparece Consumer<T>
list.forEach(System.out::println); // Iterable.forEach(Consumer)
stream.forEach(System.out::println); // Stream.forEach
optional.ifPresent(name -> log.info(name)); // Optional.ifPresent
queue.peek(System.out::println); // not a Consumer call, but the shape is the sameDonde el JDK dice "hacer algo con cada elemento", el parámetro es un Consumer<T> o un BiConsumer<K, V> para casos de dos argumentos (más notablemente Map.forEach((k, v) -> ...)).
BiConsumer<T, U>
La variante de dos argumentos:
BiConsumer<String, Integer> show = (k, v) -> System.out.println(k + " => " + v);
Map<String, Integer> scores = Map.of("alice", 1, "bob", 2);
scores.forEach(show);BiConsumer tiene el mismo default andThen. No existe BiSupplier — un Supplier de dos argumentos sería simplemente un BiFunction<T, U, R>.
Especializaciones primitivas — IntConsumer, LongConsumer, DoubleConsumer
IntConsumer printInt = System.out::println; // accepts int, no boxing
LongConsumer tally = n -> total += n;
DoubleConsumer record = d -> samples.add(d);Misma semántica de andThen. IntStream.forEach acepta un IntConsumer, razón por la que un stream primitivo puede llamar tu lambda sin boxing.
También existen ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> para el caso en que un argumento es un objeto y el otro es un primitivo — Stream.collect(Supplier, BiConsumer, BiConsumer) y sus variantes primitivas los utilizan.
Supplier<T>
@FunctionalInterface
public interface Supplier<T> {
T get(); // the only abstract method
}Esa es la interfaz completa — sin métodos default, sin andThen, sin composición. La razón es que un Supplier es la forma más simple posible: cero entradas, una salida, y lo único que puedes hacer con él es llamar a get().
Supplier<List<String>> empty = ArrayList::new;
Supplier<UUID> id = UUID::randomUUID;
Supplier<String> expensive = () -> loadFromDb();Dónde aparece Supplier<T>
Supplier es la forma del JDK de escribir lazy — "dame este valor, pero solo cuando lo necesite":
opt.orElseGet(() -> loadDefault()); // lazy default
Objects.requireNonNullElseGet(value, () -> sentinel); // lazy default for null
Stream.generate(() -> Math.random()).limit(5); // infinite stream of supplied values
logger.debug("expensive: {}", () -> serialiseGraph(state)); // lazy log argument
CompletableFuture.supplyAsync(() -> compute()); // run the supplier on another threadCada vez que aparece un Supplier<T> en el JDK, el contrato es "este valor puede que nunca sea necesario." Optional.orElseGet solo llama a get() cuando el optional está vacío; Stream.generate solo lo llama cuando se demanda el siguiente elemento. Esa pereza es el punto central — un argumento T simple ya habría sido computado cuando se invocó el método.
Especializaciones primitivas — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
IntSupplier count = () -> counter.getAndIncrement();
DoubleSupplier random = Math::random;
BooleanSupplier ready = sensor::isReady;Supplier<Boolean> funciona, pero el BooleanSupplier primitivo es lo que el JDK usa para puertas de cortocircuito (Stream.iterate, IntStream.iterate en su forma de tres argumentos toman un BooleanSupplier o IntPredicate como prueba hasNext).
Supplier versus un argumento T simple
La regla general:
- Pasa un valor cuando el costo de calcularlo es despreciable o cuando definitivamente lo necesitarás.
- Pasa un
Supplier<T>cuando el costo importa y el receptor puede no necesitar el valor.
opt.orElse(loadDefaultFromDb()); // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is emptyEsa diferencia es la razón más común por la que se prefiere orElseGet sobre orElse en código de producción.
Ejemplo práctico: Consumer.andThen, pereza de Supplier, variantes primitivas
El programa a continuación construye dos consumidores y los encadena con andThen, demuestra la diferencia de evaluación entre orElse y orElseGet con un contador, genera un stream pequeño desde un Supplier, y combina IntConsumer con IntStream.forEach para evitar el autoboxing.
Lo que se puede extraer de la ejecución:
log.andThen(store)ejecutó ambos consumidores sobre la misma entrada, en orden de declaración. El registro de auditoría mostró ambas llamadas; la cadena se convirtió en un únicoConsumer<String>que podías pasar aforEachcomo cualquier otro.- La cadena
andThenque comenzó conboomse detuvo en la excepción —neverno fue invocado nunca.andThenes secuencial, no absorbe excepciones. present.orElseGet(expensive)dejó el supplier intacto porque el optional estaba presente, mientras quepresent.orElse(expensive.get())evaluó la llamada costosa antes de que fuera necesaria. El contador de llamadas es la prueba — esa es la brecha queSupplierexiste para cubrir.Stream.generate(ids).limit(3)produjo tres UUIDs llamando aget()exactamente tres veces. El supplier es la fuente perezosa de un stream ilimitado —limites lo que hace la tubería finita.IntConsumer addse conectó directamente aIntStream.forEachy evitó el boxing de cada entero en el rango. Usa la especialización primitiva siempre que estés dentro de un stream primitivo.BooleanSupplier underFivemostró la forma que el JDK usa para la forma de tres argumentos deStream.iteratey otras puertas de "continuar hasta que" — el supplier se comprueba una vez por iteración, de forma perezosa.
Qué sigue
Ahora has visto las cuatro esquinas: Function (entrada, salida), Predicate (entrada, boolean), Consumer (entrada, sin salida), Supplier (sin entrada, salida). El siguiente capítulo, Java BinaryOperator y UnaryOperator, cierra la parte con las dos especializaciones donde cada parámetro comparte el mismo tipo — la forma que impulsa Stream.reduce, Map.merge y List.replaceAll.