Learning outcome: Implement functional interfaces with lambda expressions, choose standard function types, and explain parameter inference, return rules, and captured variables.
Many operations vary by a small piece of behavior: which marks count as a pass, how names are ordered, or what a task does. A lambda expression supplies that behavior where an API expects a functional interface. Lambdas were introduced in Java 8.
From an Anonymous Class to a Lambda
Before lambdas, a short Runnable task could be written as an anonymous class. This fragment shows both forms:
Runnable traditional = new Runnable() {
@Override
public void run() {
System.out.println("Preparing report");
}
};
Runnable concise = () -> System.out.println("Preparing report");
Both provide the behavior of Runnable.run(). Creating either object does not start a thread. Calling concise.run() executes on the current thread; new Thread(concise).start() requests execution on a new thread. Lambdas do not automatically improve performance or enable parallel execution.
Functional Interfaces
A functional interface has one abstract method contract, accounting for inherited methods and excluding methods matching public instance methods of Object. It may also contain default and static methods. @FunctionalInterface is optional but useful: the compiler rejects the interface if its abstract method contract is not functional.
A lambda needs a target type, supplied by an assignment, method argument, return context, or cast. Java uses that type to check the lambda's parameters and result. It is not a standalone named method. See Oracle's lambda tutorial.
Save as LambdaDemo.java:
public class LambdaDemo {
@FunctionalInterface
interface MarkOperation {
int apply(int mark, int bonus);
}
static int adjust(int mark, int bonus, MarkOperation operation) {
return operation.apply(mark, bonus);
}
public static void main(String[] args) {
MarkOperation addBonus = (mark, bonus) -> mark + bonus;
MarkOperation capAtHundred = (mark, bonus) -> {
int adjusted = mark + bonus;
return Math.min(adjusted, 100);
};
System.out.println(adjust(70, 5, addBonus));
System.out.println(adjust(98, 5, capAtHundred));
}
}
Output:
75
100
The adjust method receives data and a behavior. The functional interface defines the contract; each lambda implements a different calculation.
Syntax Rules
The general shape is (parameters) -> body.
| Form | Example | Meaning |
|---|---|---|
| No parameters | () -> 42 |
Supplies a value |
| One inferred parameter | mark -> mark >= 40 |
Tests one value |
| Multiple parameters | (a, b) -> a + b |
Combines two values |
| Explicit parameter types | (int a, int b) -> a + b |
Types match a compatible target interface |
| Block with a result | x -> { int y = x * 2; return y; } |
Uses statements and an explicit return |
Parentheses are required for no parameters, multiple parameters, and explicitly typed parameters. A value-returning expression body supplies its result without return. A value-returning block uses return on each path that completes normally. A void-compatible lambda can perform an action such as printing and must not return a value.
Standard Functional Interfaces
Most reusable function types live in java.util.function. Generics use reference types; primitive specializations such as IntPredicate and IntUnaryOperator avoid boxing for numeric work. See the function package API.
| Interface | Abstract method | Typical role |
|---|---|---|
Predicate<T> |
boolean test(T value) |
Keep or reject an element |
Function<T,R> |
R apply(T value) |
Transform a value |
Consumer<T> |
void accept(T value) |
Perform an action |
Supplier<T> |
T get() |
Supply a value without an argument |
UnaryOperator<T> |
T apply(T value) |
Transform a value to the same type |
BinaryOperator<T> |
T apply(T a, T b) |
Combine two values of one type |
Runnable |
void run() |
Run a task; belongs to java.lang |
Comparator<T> |
int compare(T a, T b) |
Define ordering; belongs to java.util |
Save as FunctionalTypesDemo.java:
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class FunctionalTypesDemo {
public static void main(String[] args) {
int passMark = 40;
Predicate<Integer> passed = mark -> mark >= passMark;
Function<Integer, String> label = mark -> "Mark: " + mark;
Consumer<String> display = text -> System.out.println(text);
Supplier<String> heading = () -> "Result";
display.accept(heading.get());
display.accept(label.apply(75));
System.out.println(passed.test(75));
}
}
Output:
Result
Mark: 75
true
Capturing Variables and Scope
The lambda above uses the surrounding local variable passMark. Such a local variable must be final or effectively final, meaning it is never reassigned after initialization. Adding passMark = 50; later would make the lambda invalid at compile time.
In simple terms: A lambda can use a local threshold from the surrounding method, provided that local variable does not keep changing. Pass changing data as arguments or design its ownership explicitly.
An effectively final reference may point to a mutable object; its contents are not automatically frozen. Capturing a list does not make concurrent changes to that list safe. In a lambda, this refers to the enclosing instance, unlike this in an anonymous class, which refers to the anonymous class instance.
The target interface also controls checked exceptions. For example, Runnable.run() declares no checked exceptions, so its lambda must handle an IOException internally. Callable.call() permits checked exceptions and returns a result.
Common Mistakes
| Mistake | Correction |
|---|---|
| Assigning a lambda to an interface with two unrelated abstract methods | Use a functional interface or a class implementation. |
Omitting return from a value-returning block |
Return the result explicitly. |
| Reassigning a captured local threshold | Keep it effectively final or pass it as an argument. |
| Assuming a lambda runs immediately | Invoke the interface method or pass it to an API that invokes it. |
| Putting a long algorithm inside a lambda | Extract a named method and consider a method reference. |
Practice
- Write a
Predicate<Integer>that accepts marks between 0 and 100. - Write a
Function<String,Integer>that parses trimmed numeric text. - Sort student names by length with a
Comparator<String>lambda. - Add a default method to
MarkOperationand explain why it remains functional.
Quick Check
1. Is @FunctionalInterface required for a lambda target?
No. The interface must satisfy the functional interface rules; the annotation asks the compiler to verify that design.
2. Can a Consumer return a computed mark?
No. Its accept method returns void. Use a Function or an appropriate primitive specialization for a returned result.
3. Does a lambda automatically run on another thread?
No. Execution depends on the API or method that invokes it.
Summary
Lambdas provide concise implementations of functional contracts. Select the target interface by its inputs, output, and exception behavior. Continue with method and constructor references.