Passing Functions as Data
The repetition problem
Suppose you need to filter a list of sensor readings several different ways.
static List<Integer> above100(List<Integer> readings) {
List<Integer> result = new ArrayList<>();
for (int r : readings) {
if (r > 100) result.add(r);
}
return result;
}
static List<Integer> evenOnly(List<Integer> readings) {
List<Integer> result = new ArrayList<>();
for (int r : readings) {
if (r % 2 == 0) result.add(r);
}
return result;
}
These methods are identical except for one line. Everything else — creating the list, looping, adding, returning — is duplicated.
You already know how to pass a value into a method to avoid repetition. What you need here is to pass the test itself.
Behaviour as a parameter
Java has no standalone function type. What it has is interfaces, and an object implementing a one-method interface serves the same purpose.
interface ReadingTest {
boolean matches(int reading);
}
Now the loop is written once, and the differing line becomes a parameter:
static List<Integer> filter(List<Integer> readings, ReadingTest test) {
List<Integer> result = new ArrayList<>();
for (int r : readings) {
if (test.matches(r)) { // the caller decides what this means
result.add(r);
}
}
return result;
}
Calling it means supplying an implementation:
ReadingTest above100 = new ReadingTest() {
@Override
public boolean matches(int reading) {
return reading > 100;
}
};
List<Integer> high = filter(readings, above100);
That block is an anonymous class — a class with no name, defined and instantiated in one expression. It is verbose, and Lambda Expressions exist precisely to shorten it. But this is what is really happening underneath, and seeing it once makes lambdas much less mysterious.
Why an interface is required
In some languages you can pass a function directly. In Java, the thing you pass is always an object. The interface gives that object a type, and runtime polymorphism picks the right matches implementation when filter calls it.
So “passing a function” in Java is really “passing an object with one interesting method”. Every lambda you write is compiled into something along these lines.
What this buys you
The filter method now works with tests that did not exist when it was written:
List<Integer> even = filter(readings, new ReadingTest() {
@Override public boolean matches(int r) { return r % 2 == 0; }
});
List<Integer> inRange = filter(readings, new ReadingTest() {
@Override public boolean matches(int r) { return r >= 50 && r <= 150; }
});
One loop, any number of behaviours. The pattern generalises beyond filtering — anywhere you find near-identical methods differing by a line or two, the difference can usually become a parameter.
Functional interfaces
An interface with exactly one abstract method is called a functional interface, and it is the shape Java uses for this everywhere.
@FunctionalInterface
interface ReadingTest {
boolean matches(int reading);
}
The annotation is optional but worth adding. It makes the compiler reject the interface if someone later adds a second abstract method — which would silently break every lambda written against it.
Java already ships the common shapes, so you rarely define your own. Those are covered in Supplier and the Functional Interfaces.
Deferring work
There is a second use, distinct from removing duplication: describing work now and running it later.
interface Action {
void run();
}
class ActionQueue {
private final Queue<Action> pending = new ArrayDeque<>();
void schedule(Action action) {
pending.offer(action); // stored, not executed
}
void runAll() {
while (!pending.isEmpty()) {
pending.poll().run(); // executed now
}
}
}
The action is created at one point in the program and executed at another. That separation is the foundation of event handlers, callbacks, and the Command Pattern — and it is how FRC’s command-based framework schedules robot behaviour.
Two distinct motivations
- Removing duplication — the surrounding code is fixed, one step varies. Pass the varying step in.
- Deferring execution — you know what should happen but not yet when. Store the behaviour and run it later.
Both use the same mechanism. Recognising which one you need helps you name things sensibly.
Returning behaviour
A method can also hand back an implementation, which lets you build behaviour from parameters:
static ReadingTest greaterThan(int threshold) {
return new ReadingTest() {
@Override
public boolean matches(int reading) {
return reading > threshold; // captures threshold
}
};
}
List<Integer> high = filter(readings, greaterThan(100));
List<Integer> veryHigh = filter(readings, greaterThan(500));
The returned object remembers threshold — a captured variable. Note the restriction: a captured local variable must be effectively final, meaning you cannot reassign it after capture. Java enforces this at compile time.
Common mistakes
- Adding a second abstract method to a functional interface, breaking every lambda using it.
@FunctionalInterfaceprevents this. - Reassigning a captured local variable, which is a compile error.
- Calling the behaviour when you meant to store it.
schedule(action)stores;action.run()executes. - Over-applying the pattern. If a method has exactly one caller and one behaviour, parameterising it adds indirection for no benefit.
Practice
Use anonymous classes throughout — lambdas come in the next lesson.
- Define a
ReadingTestinterface and afiltermethod, then filter a list three different ways. - Define a
Transformerinterface withint apply(int value)and write amapmethod that applies it to every element of a list. - Write a
greaterThan(int)method that returns aReadingTest, and use it twice with different thresholds. - Define an
Actioninterface and anActionQueuethat stores actions and runs them in order. - Write a
sorthelper that takes a list and a comparison interface you define yourself, so the caller decides the ordering.
Hints
- As written above.
- Same loop shape; call
applyand add the result. - The returned anonymous class captures the parameter.
- Store in an
ArrayDeque;runAllpolls and callsrun(). - Your interface needs a method returning a negative number, zero, or a positive number. Then any simple sort works using it for comparisons. This is exactly how
Comparatoris built.