Supplier and the Functional Interfaces

intermediate30 min

Stop defining your own

In the first lesson of this module you wrote a ReadingTest interface. It was useful for understanding the mechanism, but you should almost never need to write one — Java ships the common shapes in java.util.function.

A functional interface is fully described by how many arguments it takes and whether it returns anything. There are only a few useful combinations.

The interfaces you will actually use

InterfaceMethodTakesReturnsUse for
Supplier<T>get()nothinga TProducing or reading a value later
Consumer<T>accept(t)a TnothingDoing something with a value
Function<T,R>apply(t)a Tan RTransforming a value
Predicate<T>test(t)a TbooleanTesting a condition
Runnablerun()nothingnothingAn action with no input or output
BiFunction<T,U,R>apply(t,u)two valuesan RCombining two values
UnaryOperator<T>apply(t)a Ta TSame-type transformation

So the earlier filter method should have been written:

static <T> List<T> filter(List<T> items, Predicate<T> test) {
    List<T> result = new ArrayList<>();
    for (T item : items) {
        if (test.test(item)) result.add(item);
    }
    return result;
}

List<Integer> high = filter(readings, r -> r > 100);

Using the standard interface means it composes with everything else in the library, and any reader recognises it immediately.

Supplier in detail

Supplier deserves its own treatment because it does something the others do not: it defers when a value is read.

Supplier<Double> reading = () -> sensor.getDistance();

Nothing has been read yet. The sensor is queried only when someone calls reading.get() — and every call reads it again, returning the current value.

Contrast passing the value itself:

double snapshot = sensor.getDistance();     // read once, frozen
Supplier<Double> live = () -> sensor.getDistance();   // read on demand

The first is a number from a moment in the past. The second is a way to ask “what is it now?”.

Why this matters in robot code

Almost every value a robot cares about changes constantly — joystick positions, encoder counts, sensor distances. Code written when the robot starts must reference values that will not exist until later.

// wrong: captures the joystick position at construction, once, forever
new DriveCommand(joystick.getY());

// right: the command reads the current position each cycle
new DriveCommand(() -> joystick.getY());

This distinction is behind most “why does my robot only use the first value” confusion. If your command needs a value that changes, it needs a Supplier, not a double.

DoubleSupplier avoids boxing when the value is a primitive, which matters in a loop running fifty times a second:

DoubleSupplier reading = () -> sensor.getDistance();
double now = reading.getAsDouble();

Lazy and expensive work

The second use for Supplier: skipping work that might not be needed.

// the message is built even when the condition is false
log(isDebug, "state: " + buildExpensiveReport());

// the message is built only if the method decides to use it
logLazy(isDebug, () -> "state: " + buildExpensiveReport());

static void logLazy(boolean enabled, Supplier<String> message) {
    if (enabled) {
        System.out.println(message.get());
    }
}

Java evaluates arguments before calling a method, so the first version always pays for the report. Wrapping it in a Supplier moves that decision inside the method.

The standard library uses this in Optional:

String name = maybeName.orElseGet(() -> computeDefault());   // lazy
String name2 = maybeName.orElse(computeDefault());           // always computes

orElseGet takes a Supplier and only calls it when the Optional is empty. orElse takes a value, so the default is computed either way. When the default is expensive, that difference is real.

Primitive variants

Generics cannot hold primitives, so Function<Integer, Integer> boxes every value. For hot loops Java provides unboxed versions:

BoxedPrimitive version
Supplier<Double>DoubleSupplier
Consumer<Integer>IntConsumer
Predicate<Integer>IntPredicate
Function<Integer,Integer>IntUnaryOperator
BiFunction<Double,Double,Double>DoubleBinaryOperator

For ordinary code the boxed versions are fine and clearer. Reach for the primitive ones only when profiling shows the boxing matters.

Composing them

Several of these interfaces combine, which saves writing intermediate variables:

Predicate<Integer> positive = r -> r > 0;
Predicate<Integer> small = r -> r < 100;

Predicate<Integer> both = positive.and(small);
Predicate<Integer> either = positive.or(small);
Predicate<Integer> not = positive.negate();

Function<Integer, Integer> doubled = x -> x * 2;
Function<Integer, String> describe = x -> "value: " + x;

Function<Integer, String> combined = doubled.andThen(describe);
System.out.println(combined.apply(5));      // "value: 10"

andThen runs the first, then feeds its result to the second. compose does the reverse order. Getting them backwards is a common slip — a.andThen(b) means a first.

Common mistakes

  • Writing your own interface when a standard one fits.
  • Passing a value where a Supplier is needed, freezing it at the wrong moment.
  • Calling get() when you meant to store the supplier. supplier.get() reads now; supplier is the ability to read later.
  • Using orElse with an expensive default instead of orElseGet.
  • Confusing andThen with compose.
  • Boxing in a tight loop where a primitive variant exists.
  • Assuming a Supplier caches. Each get() runs the body again. Memoise deliberately if you need caching.

Practice

  1. Rewrite your ReadingTest filter using Predicate<Integer>.
  2. Write a method taking a Consumer<String> and calling it with three different names.
  3. Write a logLazy(boolean, Supplier<String>) method and demonstrate that the message is not built when the flag is false — print something inside the supplier to prove it.
  4. Build a Function<Integer,String> by composing a doubling function with a formatting function, and verify the order.
  5. Write a Supplier<Double> reading from a mutable field, then change the field and call get() again to confirm you see the new value.
Hints
  1. Swap the interface and call test.
  2. Parameter type Consumer<String>; call accept three times.
  3. Put a System.out.println("building") inside the supplier body. With the flag false it should never print.
  4. doubled.andThen(describe). Try compose too and note the difference.
  5. This is the key exercise — it shows a supplier reads live rather than capturing a snapshot.

Next

Related