Key Takeaways
- Java lambda expressions provide concise implementations of a functional interface's single abstract method contract.
- A lambda needs a target type supplied by its context, such as a variable declaration or method parameter.
- Use Consumer for actions, Predicate for tests, Function for transformations, and Supplier for producing values.
- Lambdas are lexically scoped. Captured local variables must be final or effectively final, but the objects they reference can still be mutable.
Java lambda expressions let you supply the behavior of a functional interface without writing a named implementation class or an anonymous class. They're useful when you want to pass an action, test, or transformation to another method.
The practical idea is passing behavior alongside data. That's the part of functional programming we'll use here—not a requirement to rewrite your application in a different style.
What Are Lambdas in Java?
Start with an interface that has one abstract method:
@FunctionalInterface
interface Print {
void print(String message);
}
public class MainClass {
public static void main(String[] args) {
Print myPrinter = message -> System.out.println(message);
myPrinter.print("Hello lambdas!");
}
}The lambda supplies the implementation of Print.print(String). Assigning it creates or obtains an implementation instance; the printing happens when you call myPrinter.print(...), not when you assign the lambda.
Save this listing as MainClass.java, then compile and run it with a JDK. Treat each subsequent complete listing as a separate program named after its public class.
javac MainClass.java
java MainClassWhat Makes an Interface Functional?
A functional interface has one abstract method contract. It may also contain default methods and static methods: neither adds another abstract method for your lambda to implement. Declarations matching public instance methods of Object, such as equals(Object), don't count toward that contract.
The @FunctionalInterface annotation is optional, but useful: it tells the compiler to reject the declaration if the interface isn't functional. One additional restriction is that a sealed interface cannot be a functional interface.
Understanding the Target Type
A lambda has no standalone type determined solely by its syntax. Java uses its context—the target type—to determine the functional interface it implements. In the first example, Print myPrinter supplies that type, so Java knows the parameter is a String and the method returns void.
The target can also come from a method parameter or an explicit cast. Two unrelated functional interfaces can accept the same lambda syntax; matching parameter and return types doesn't make those interfaces interchangeable.
The java.util.function Package
You usually don't need to invent an interface for every callback. The java.util.function package supplies standard interfaces for common input/output shapes.
| Interface | Abstract method | Use it for |
|---|---|---|
Consumer<T> | void accept(T t) | Performing an action on one value. |
Predicate<T> | boolean test(T t) | Testing whether a value meets a condition. |
Function<T, R> | R apply(T t) | Transforming an input into a result. |
Supplier<T> | T get() | Producing a value without an argument. |
IntBinaryOperator | int applyAsInt(int left, int right) | Combining two primitive integers into an integer result. |
In Function<T, R>, T represents the input type and R the result type. For example, Function<String, Integer> takes a string and produces an integer. Our guide to Java generics and type inference explains these type parameters in more detail.
Java Lambda Syntax
A lambda has three parts: parameters, the -> token, and an expression or block body.
parameters arrow body
(a, b) -> a + b
(a, b) -> { return a + b; }Zero, One, or Multiple Parameters
- Zero parameters: use empty parentheses, as in
() -> "Hello". - One inferred parameter: parentheses are optional, as in
message -> System.out.println(message). - Explicit parameter types: include parentheses, as in
(String message) -> message.strip(). - Multiple parameters: use parentheses and commas. Declare all parameter types or let Java infer all of them; don't mix those forms.
Expression Bodies and Block Bodies
An expression body doesn't use the return keyword. For a value-returning interface, the expression supplies the result. For a void interface, an appropriate statement expression—such as a method call—can supply the action.
A block body uses braces and can contain multiple statements. If the interface requires a result, the block cannot fall through without returning a value: each completing path must return one, though a path may throw an exception instead. A void block needs no return statement, although a bare return; is allowed.
This complete example puts the common forms together. String.strip() removes leading and trailing whitespace; String.isBlank() checks for an empty or whitespace-only string.
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class SyntaxDemo {
public static void main(String[] args) {
Supplier<String> greeting = () -> "Hello";
Consumer<String> printer = message -> System.out.println(message);
Predicate<String> hasText = (message) -> !message.isBlank();
Function<String, String> clean = (String message) -> message.strip();
IntBinaryOperator add = (int a, int b) -> a + b;
Function<String, Integer> length = message -> {
String stripped = message.strip();
return stripped.length();
};
printer.accept(clean.apply(greeting.get()));
System.out.println(hasText.test(" "));
System.out.println(add.applyAsInt(2, 2));
System.out.println(length.apply(" Java "));
}
}The program prints Hello, false, 4, and 4, each on its own line.
Passing Lambdas as Method Arguments
The useful step isn't merely storing a lambda in a variable. It's letting a method's caller choose some behavior. Here, calculate accepts a custom MathOp, while matches accepts a standard Predicate<String>.
import java.util.function.Predicate;
public class BehaviorsDemo {
@FunctionalInterface
interface MathOp {
int operate(int a, int b);
}
static int calculate(int a, int b, MathOp operation) {
return operation.operate(a, b);
}
static boolean matches(String value, Predicate<String> condition) {
return condition.test(value);
}
public static void main(String[] args) {
System.out.println(calculate(2, 2, (a, b) -> a + b));
System.out.println(matches("Java", text -> !text.isBlank()));
}
}The first call prints 4; the predicate call prints true. The parameter types supply the target types for both lambdas.
For ordinary two-integer arithmetic, IntBinaryOperator already provides the same input/output shape. Prefer standard interfaces unless a domain-specific interface name makes your API clearer.
Why Use Lambdas in Java?
Less Boilerplate Than Anonymous Classes
Compare these two implementations of the original Print interface. The first declares an anonymous class; the second supplies the behavior directly.
// Fragment: inside main(), using the Print interface defined earlier.
Print anonymousPrinter = new Print() {
@Override
public void print(String message) {
System.out.println(message);
}
};
Print lambdaPrinter = message -> System.out.println(message);
anonymousPrinter.print("Hello anonymous classes!");
lambdaPrinter.print("Hello lambdas!");They're not interchangeable in every situation. An anonymous class has its own this and can declare fields and additional methods. A lambda doesn't introduce a new this; in an instance context, it uses the enclosing instance. See Java nested classes when you need the class-based alternative.
Less syntax isn't a guarantee of faster execution or lower memory use. Choose lambdas for clarity, then measure performance where it matters.
Collection Iteration and Stream Transformations
Calling list.forEach(...) performs an action for each element immediately. This method comes from Iterable; using it doesn't create a stream or make the operation part of the Streams API.
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
public class CollectionsDemo {
public static void main(String[] args) {
List<Character> letters = List.of('a', 'b', 'c');
letters.forEach(letter -> System.out.println(letter));
List<String> names = List.of(" Ada ", " ", "Grace", " Linus ");
Predicate<String> hasText = name -> !name.isBlank();
Function<String, String> normalize = name -> name.strip();
List<String> cleaned = names.stream()
.filter(hasText)
.map(normalize)
.toList();
System.out.println(cleaned);
}
}The letters print individually. The final line prints [Ada, Grace, Linus].
The pipeline expresses a transformation: filter retains names that pass the predicate, map applies the normalization function, and toList gathers the results. The pipeline doesn't remove or replace entries in the original list.
filter and map are lazy intermediate operations: they describe work without traversing the source yet. A terminal operation, here toList(), triggers processing. The stream package documentation explains this execution model; our Java Collections Framework guide covers the underlying containers.
Java Method References
When a lambda only forwards its arguments to an existing method, a method reference often communicates that intent more clearly. Like a lambda, a method reference needs a compatible functional-interface target type.
import java.util.function.Consumer;
public class MethodReferencesDemo {
static void systemPrint(String message) {
System.out.println(message);
}
public static void main(String[] args) {
Consumer<String> forwarding = message -> System.out.println(message);
Consumer<String> printer = System.out::println;
Consumer<String> customPrinter = MethodReferencesDemo::systemPrint;
forwarding.accept("Forwarding lambda");
printer.accept("Method reference");
customPrinter.accept("Static method reference");
}
}System.out::println refers to a method on an existing object. MethodReferencesDemo::systemPrint refers to a static method. You can also replace the earlier normalization lambda with String::strip, which calls strip() on the supplied string.
Keep the lambda when it adds meaningful logic, such as combining conditions or supplying an extra argument. Shorter isn't automatically clearer.
Lambda Scope and Effectively Final Variables
Lambdas are lexically scoped: names are resolved according to the surrounding source-code context. A lambda parameter or local variable cannot redeclare an enclosing local variable that's still in scope. Anonymous classes have different shadowing rules.
A captured local variable must be declared final or be effectively final. For a local initialized in its declaration, effectively final means you never reassign, increment, or decrement it afterward—even outside the lambda. The variable must also be definitely assigned before capture.
import java.util.ArrayList;
import java.util.List;
public class CaptureDemo {
@FunctionalInterface
interface Print {
void print();
}
public static void main(String[] args) {
String message = "Hello world!";
Print myPrinter = () -> {
System.out.println(message);
};
List<String> messages = new ArrayList<>();
Print remember = () -> {
messages.add(message);
};
myPrinter.print();
remember.print();
System.out.println(messages);
// message = "Changed"; // Uncommenting breaks effective finality.
// messages = new ArrayList<>(); // This would also break it.
}
}message isn't declared final; it's effectively final because it isn't reassigned. The messages variable also remains effectively final even though remember changes the referenced list. Capturing a reference does not freeze or copy the object it points to.
Common Mistakes
- Expecting var to supply the target:
var printer = message -> System.out.println(message);doesn't compile. Declare the functional-interface type, such asConsumer<String>, instead. - Ignoring overload ambiguity: overloads accepting unrelated functional interfaces with matching parameter and return shapes can make a lambda call ambiguous. Assign the lambda to a variable of the intended interface type, or cast it at the call site.
- Forgetting checked exceptions: a lambda must obey its interface's exception contract.
Consumer.acceptdoesn't declare checked exceptions; handle them in the body or choose an interface whose method declares them. - Hiding a whole algorithm in a callback: when a lambda becomes difficult to name, test, or scan, extract a method. Use a method reference if it makes the call site clearer.
FAQ
What is a lambda expression in Java?
A lambda expression supplies the implementation of a functional interface's single abstract method contract. You can assign it to a compatible interface variable or pass it directly to a method that accepts that interface.
What is the target type of a Java lambda?
The target type is the functional-interface type required by the lambda's context, such as a variable declaration, method parameter, or cast. Java uses it to check the lambda's parameters and result.
What makes an interface a functional interface?
An interface that isn't sealed and has one abstract method contract is a functional interface. Default and static methods don't add abstract requirements, and declarations matching public instance methods of Object don't count toward that contract.
What does effectively final mean in a Java lambda?
A local variable initialized in its declaration is effectively final if it is never reassigned, incremented, or decremented afterward. The object referenced by that variable may still be mutable; the restriction applies to the variable, not the object's contents.
What is the difference between a lambda and an anonymous class in Java?
A lambda supplies behavior for a functional interface and uses the enclosing meaning of this. An anonymous class defines a class with its own this and can declare fields and additional methods; both can capture final or effectively final local variables.
Further Reading
- Dev.java: Writing Your First Lambda Expression — identifying target types and implementing functional interfaces.
- java.util.function API reference — standard interfaces and primitive specializations.
- Dev.java: Writing Lambda Expressions as Method References — static, bound, unbound, and constructor references.
- Java Language Specification: Expressions — precise lambda syntax, typing, and runtime rules.
- Java Language Specification: Interfaces — the formal functional-interface definition.
- Java Language Specification: Types, Values, and Variables — final references and effective finality.
- Iterable API reference — the collection-style forEach contract.
- Stream API reference — filter, map, terminal operations, and toList.
