Behavior Parameterization and Functional Interfaces in Java
Part 2 of 9 in Functional Java Unleashed
Before Java 8, if you wanted to filter a list based on different criteria, you wrote a new method for each one: getTodoTasks(), getInProgressTasks(), getHighPriorityTasks(). Each method repeated the same iteration logic with only the condition changing. Every new criterion meant more boilerplate and more methods to maintain.
Behavior parameterization solves this by treating the criterion itself as a value — something you can pass around, compose, and substitute at call sites. Java 8 introduced functional interfaces (interfaces with exactly one abstract method) as the type-safe contract for this pattern, and lambda expressions as the concise syntax to implement them.
This post walks through a concrete example: filtering a list of tasks by various criteria — first the old way with duplicated methods, then the parameterized way using Predicate, Function, Consumer, and Supplier from java.util.function.
The code
The demo defines a small domain model (Task) and six sample tasks. It shows two approaches to filtering:
Before: one method per criterion, each repeating the same loop with a different condition.
After: a single filterWithPredicate method that accepts any Predicate<Task> — and then passes in lambdas for every criterion.
The full source lives below:
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.Consumer;
public class BehaviorParameterization {
record Task(String name, int priority, String status, int estimatedHours) {}
static final List<Task> TASKS = List.of(
new Task("Write unit tests", 1, "TODO", 8),
new Task("Fix login bug", 2, "IN_PROGRESS", 3),
new Task("Update documentation", 3, "TODO", 4),
new Task("Refactor service layer", 1, "DONE", 12),
new Task("Setup CI pipeline", 2, "TODO", 6),
new Task("Code review PR #42", 3, "IN_PROGRESS", 2)
);
// Custom functional interface — exactly one abstract method.
// The @FunctionalInterface annotation is optional; the compiler enforces single-method.
@FunctionalInterface
interface TaskFilter {
boolean test(Task t);
}
// -- BEFORE: duplicated methods, no behavior parameterization --
static List<Task> getTodoTasks() {
List<Task> result = new ArrayList<>();
for (Task t : TASKS)
if ("TODO".equals(t.status())) result.add(t);
return result;
}
static List<Task> getInProgressTasks() {
List<Task> result = new ArrayList<>();
for (Task t : TASKS)
if ("IN_PROGRESS".equals(t.status())) result.add(t);
return result;
}
static List<Task> getHighPriorityTasks() {
List<Task> result = new ArrayList<>();
for (Task t : TASKS)
if (t.priority() <= 1) result.add(t);
return result;
}
// -- Custom filter method accepting any TaskFilter --
static List<Task> filterTasks(String label, List<Task> tasks, TaskFilter predicate) {
List<Task> result = new ArrayList<>();
for (Task t : tasks)
if (predicate.test(t)) result.add(t);
return result;
}
// -- AFTER: built-in Predicate + lambda --
static List<Task> filterWithPredicate(String label, List<Task> tasks,
Predicate<Task> predicate) {
System.out.println("=== " + label + " ===");
var result = new ArrayList<Task>();
for (Task t : tasks)
if (predicate.test(t)) result.add(t);
return result;
}
record Metric(String name, double value) {}
static <T> List<Metric> computeMetrics(
List<T> items, Function<T, Double> extractValue, String metricName) {
var metrics = new ArrayList<Metric>();
for (T item : items)
metrics.add(new Metric(metricName + ": " + extractValue.apply(item),
extractValue.apply(item)));
return metrics;
}
public static void main(String[] args) {
// OLD way: three separate methods doing the same thing differently
System.out.println("=== BEFORE: Duplicated methods ===");
System.out.println("\nTodo tasks:");
getTodoTasks().forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
System.out.println("\nIn-progress tasks:");
getInProgressTasks().forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
System.out.println("\nHigh-priority tasks:");
getHighPriorityTasks().forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
// NEW way: one method, any behavior as a lambda
var todoTasks = filterWithPredicate("TODO tasks", TASKS,
t -> "TODO".equals(t.status()));
todoTasks.forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
var highPriorityTasks = filterWithPredicate("High-priority tasks (p<=1)", TASKS,
t -> t.priority() <= 1);
highPriorityTasks.forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
var longTasks = filterWithPredicate("Tasks needing >= 4 hours", TASKS,
t -> t.estimatedHours() >= 4);
longTasks.forEach(t ->
System.out.printf(" %-25s priority=%d status=%s hours=%d%n",
t.name(), t.priority(), t.status(), t.estimatedHours()));
var todoAndHigh = filterWithPredicate("TODO + High-priority (combined)", TASKS,
t -> "TODO".equals(t.status()) && t.priority() <= 1);
if (todoAndHigh.isEmpty()) {
System.out.println(" (none)");
} else {
todoAndHigh.forEach(t ->
System.out.printf(" %-25s priority=%d status=%s%n", t.name(), t.priority(), t.status()));
}
// Function: extracts a value from each item
List<Metric> hoursMetrics = computeMetrics(TASKS,
t -> (double) t.estimatedHours(), "Est. Hours");
hoursMetrics.forEach(m -> System.out.printf(" %-30s = %.1f%n", m.name(), m.value()));
// Supplier: lazily produces a single value
Supplier<Task> nextTaskSupplier = () -> new Task("New feature", 2, "TODO", 8);
var newTask = nextTaskSupplier.get();
System.out.printf(" Produced: %-25s priority=%d status=%s%n",
newTask.name(), newTask.priority(), newTask.status());
// Consumer: performs a side-effect for each item
Consumer<Task> printer = t ->
System.out.printf(" %s [p=%d, %s, %dh]%n",
t.name(), t.priority(), t.status(), t.estimatedHours());
TASKS.forEach(printer);
// Custom TaskFilter + lambda (same contract as Predicate)
List<Task> filtered = filterTasks("Custom filter: TODO tasks", TASKS,
t -> "TODO".equals(t.status()));
System.out.printf(" Found %d task(s):\n", filtered.size());
filtered.forEach(printer);
}
}
Running it
The output shows the contrast clearly. The BEFORE section calls three separate methods — getTodoTasks(), getInProgressTasks(), getHighPriorityTasks() — each iterating over the same six tasks with only the condition changed. That’s three methods doing essentially the same work.
The AFTER section replaces all of that with calls to filterWithPredicate using four different lambda expressions:
t -> "TODO".equals(t.status())filters by status — returns 3 tasks (Write unit tests, Update documentation, Setup CI pipeline).t -> t.priority() <= 1filters by priority — returns 2 tasks (Write unit tests, Refactor service layer).t -> t.estimatedHours() >= 4filters by duration — returns 4 tasks (all except Fix login bug and Code review PR #42).t -> "TODO".equals(t.status()) && t.priority() <= 1combines two criteria — returns exactly 1 task (Write unit tests), showing how behaviors compose.
The remaining sections demonstrate other functional interfaces:
Function<Task, Double>(computeMetrics) extracts a numeric value from each task and produces six metric rows with values matching: 8.0, 3.0, 4.0, 12.0, 6.0, 2.0.Supplier<Task>lazily produces one newTaskon demand — the output shows it created a task named “New feature” with priority 2 and status TODO.Consumer<Task>(printer) performs a side-effect (printing) for each of the six tasks.- The final block uses the custom
TaskFilterfunctional interface with a lambda to filter, confirming three TODO tasks were found — identical to what the built-inPredicateproduced above.
The key insight is that the filtering logic stopped being code in methods and became data passed as arguments. The iteration pattern never changed — only the criterion did. And the criterion was just an expression on one line: t -> <condition>.
Takeaway
Behavior parameterization turns a family of similar methods into one flexible method whose behavior is supplied at call time. Functional interfaces (Predicate, Function, Consumer, Supplier) define the type-safe contract — the compiler ensures your lambda matches the expected signature — and lambda expressions make passing executable logic as lightweight as passing a value. The same pattern underlies streams, comparators, event handlers, and countless other APIs that accept behavior as an argument.
This matters because it lets you write once (the iteration/filtering logic) and express variation only where it differs (the predicate), making code shorter, composable, and easier to test.