Supplier and the Functional Interfaces
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
| Interface | Method | Takes | Returns | Use for |
|---|---|---|---|---|
Supplier<T> | get() | nothing | a T | Producing or reading a value later |
Consumer<T> | accept(t) | a T | nothing | Doing something with a value |
Function<T,R> | apply(t) | a T | an R | Transforming a value |
Predicate<T> | test(t) | a T | boolean | Testing a condition |
Runnable | run() | nothing | nothing | An action with no input or output |
BiFunction<T,U,R> | apply(t,u) | two values | an R | Combining two values |
UnaryOperator<T> | apply(t) | a T | a T | Same-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:
| Boxed | Primitive 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
Supplieris needed, freezing it at the wrong moment. - Calling
get()when you meant to store the supplier.supplier.get()reads now;supplieris the ability to read later. - Using
orElsewith an expensive default instead oforElseGet. - Confusing
andThenwithcompose. - Boxing in a tight loop where a primitive variant exists.
- Assuming a
Suppliercaches. Eachget()runs the body again. Memoise deliberately if you need caching.
Practice
- Rewrite your
ReadingTestfilter usingPredicate<Integer>. - Write a method taking a
Consumer<String>and calling it with three different names. - 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. - Build a
Function<Integer,String>by composing a doubling function with a formatting function, and verify the order. - Write a
Supplier<Double>reading from a mutable field, then change the field and callget()again to confirm you see the new value.
Hints
- Swap the interface and call
test. - Parameter type
Consumer<String>; callacceptthree times. - Put a
System.out.println("building")inside the supplier body. With the flag false it should never print. doubled.andThen(describe). Trycomposetoo and note the difference.- This is the key exercise — it shows a supplier reads live rather than capturing a snapshot.