Lambda Expressions — Syntax, Target Typing, Variable Capture, and Method References
In this tutorial, you will learn about Lambda Expressions. We cover key concepts, practical examples, and best practices to help you master this topic.
Lambda expressions in Java enable Functional Programming by providing concise syntax for implementing functional interfaces. Introduced in Java 8, lambdas transformed Java programming by allowing behavior to be passed as data — making code more expressive and reducing boilerplate compared to anonymous classes.
What You'll Learn
- Lambda expression syntax and variations
- Target typing and functional interfaces
- Variable capture (effectively final)
- Method references (
::operator)
Why It Matters
Lambdas are the foundation of the Stream API, Optional, CompletableFuture, and virtually every modern Java library. Without understanding lambdas, you cannot effectively use Java 8+ APIs.
Real-World Use
Sorting with Comparator.comparing(), event handlers (JavaFX), thread creation (new Thread(() -> { ... })), and every Stream pipeline relies on lambdas.
Lambda Syntax
// Full syntax: (parameters) -> { body }
(int a, int b) -> { return a + b; }
// Single parameter, no type
a -> a * 2
// No parameters
() -> System.out.println("Hello")
// Multiple statements
(String s) -> {
String upper = s.toUpperCase();
System.out.println(upper);
return upper;
}
Concise Forms
// Type inference — compiler knows types from context
Comparator<String> byLength = (s1, s2) -> Integer.compare(s1.length(), s2.length());
// Single expression — no braces or return needed
Function<String, Integer> lengthFunc = s -> s.length();
// Multiple parameters — parentheses required
BinaryOperator<Integer> sum = (a, b) -> a + b;
Target Typing
The compiler infers the lambda's type from the context:
// Assigning to a functional interface variable
Predicate<String> isEmpty = s -> s.isEmpty();
// Passing as an argument
list.sort((a, b) -> a.compareTo(b));
// Returning from a method
public Predicate<Integer> isGreaterThan(int threshold) {
return n -> n > threshold;
}
The @FunctionalInterface Annotation
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
Variable Capture
Lambdas can capture variables from the enclosing scope, but they must be effectively final:
String prefix = "Hello: ";
Function<String, String> greet = name -> prefix + name; // OK (prefix is effectively final)
int multiplier = 2;
Function<Integer, Integer> doubler = x -> x * multiplier; // OK
// ERROR: variable must be effectively final
int counter = 0;
Runnable increment = () -> counter++; // COMPILE ERROR
Why Effectively Final?
Lambdas capture the variable's value at the point of creation. If the variable could change, the lambda's behavior would be ambiguous. This also enables the JVM to avoid heap-allocating captured variables.
Mutable Capture Workaround
Use an array or AtomicInteger to work around the restriction:
int[] counter = {0};
Runnable increment = () -> counter[0]++; // OK (the reference is final, the content is mutable)
// Or use AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
Runnable increment = counter::incrementAndGet;
this in Lambdas vs Anonymous Classes
In an anonymous class, this refers to the anonymous instance. In a lambda, this refers to the enclosing instance:
public class ThisDemo {
private String value = "Outer";
public void test() {
Runnable anonymous = new Runnable() {
private String value = "Anonymous";
@Override
public void run() {
System.out.println(this.value); // "Anonymous"
}
};
Runnable lambda = () -> {
System.out.println(this.value); // "Outer"
};
}
}
Method References
Method references are shorthand for lambdas that call a single method:
// Lambda
list.forEach(s -> System.out.println(s));
// Method reference
list.forEach(System.out::println);
Four Types
| Type | Syntax | Lambda Equivalent |
|---|---|---|
| Static method | ClassName::staticMethod |
(args) -> ClassName.staticMethod(args) |
| Instance method on a specific object | instance::method |
(args) -> instance.method(args) |
| Instance method on an arbitrary object | ClassName::instanceMethod |
(obj, args) -> obj.instanceMethod(args) |
| Constructor | ClassName::new |
(args) -> new ClassName(args) |
// Static method
Function<String, Integer> parseInt = Integer::parseInt;
// Instance method on specific object
Consumer<String> print = System.out::println;
// Instance method on arbitrary object
Function<String, Integer> getLength = String::length;
Comparator<String> byCase = String::compareToIgnoreCase;
// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;
Function<String, Person> personCreator = Person::new;
Common Mistakes
- Using lambdas for multi-line logic. If a lambda has more than 3-4 lines, extract it into a named method and use a method reference.
- Modifying captured variables. The effectively final restriction catches this at compile time. Use
AtomicIntegeror an array instead. - Confusing lambda with anonymous class
this.thisin a lambda refers to the enclosing instance, not the lambda itself. - Omitting parentheses for zero-parameter lambdas.
() -> 42is correct;-> 42is not. - Using lambdas when method references are clearer.
Person::getNameis better thanp -> p.getName().
Practice Questions
1. What is a functional interface?
An interface with exactly one abstract method. Lambdas can be used to instantiate functional interfaces.
2. What does "effectively final" mean for lambda captures?
A variable is effectively final if it is not reassigned after initialization. Lambdas can capture such variables.
3. How do method references differ from lambdas?
Method references are a shorthand for lambdas that call exactly one existing method. They are more concise but less flexible than lambdas.
4. Why does this behave differently in lambdas vs anonymous classes?
In anonymous classes, this refers to the anonymous class instance. In lambdas, this refers to the enclosing class — lambdas do not introduce a new scope.
5. Can a lambda have multiple abstract methods?
No. Lambda expressions are designed for functional interfaces (single abstract method). For multiple methods, use an anonymous class.
Challenge Question:
Create a fluent logger API using functional interfaces:
Logger logger = Logger.withPrefix("[APP] ");
logger.info("Starting application");
logger.info("User logged in: {}", "Alice");
Define Logger with methods that accept lambdas and Supplier for lazy evaluation. The info method should only evaluate the message supplier if the log level is enabled.
FAQ
Mini Project
Write a program LambdaDemo.java that:
- Creates a list of words and sorts them by length using a lambda
- Filters words starting with "A" using
Predicatelambda andStream.filter() - Transforms each word to uppercase using
Functionlambda - Creates a custom
@FunctionalInterfaceStringProcessorwith methodString Process(String input), and implements it with three different lambdas: trim, reverse, and uppercase - Uses constructor reference
ArrayList::newto create a new list - Demonstrates effectively final capture with a counter
- Shows the
thisdifference between lambda and anonymous class
What's Next
Lambdas enable functional programming, and the Stream API is their primary use case. Lesson 34 covers Stream API — creating streams, intermediate operations (map, filter, flatMap), terminal operations (collect, reduce, forEach), and parallel streams.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro