1. Introduction
Let’s be honest: writing anonymous classes for one or two lines of code is like hiring a huge truck to deliver a single bun from the bakery to the shop.
For example, if you want to sort a list of strings by length, before Java 8 you had to write this:
List<String> list = Arrays.asList("apple", "banana", "kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
The task is simple, yet the code takes up half a screen. It’s especially annoying when there are many such operations: the code becomes “noisy,” and the essence of the task gets lost. Developers cried, suffered, and then invented lambda expressions. We’ve already studied them a bit; now we’ll review and deepen our knowledge.
What is a lambda expression
A lambda expression is a compact way to implement a functional interface, i.e., an interface with a single abstract method (for example, Comparator, Runnable, Consumer, and many others).
Simply put, a lambda expression lets you write a function “on the fly,” right where it’s needed, without declaring a separate class or method.
General syntax:
(parameters) -> { body }
Examples:
- (a, b) -> a + b — a function that adds two numbers
- x -> x * x — a function that squares a number
- () -> System.out.println("Hello!") — a function with no parameters
Relationship with functional interfaces:
A lambda expression can always be assigned to a variable of a functional interface type or passed as an argument to a method that expects such an interface.
2. Syntax of lambda expressions
No parameters
Runnable r = () -> System.out.println("Hello, world!");
r.run(); // Prints: Hello, world!
Single parameter
If there’s a single parameter, you can omit the parentheses:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java is cool!");
Multiple parameters
Parentheses are required:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Single-expression body
If the body consists of a single expression, braces and return are not needed:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Block body
If you need several statements, use braces and return (if there’s a return value):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Parameter types
Most of the time you can omit parameter types—the compiler infers them from context. But if you want, you can specify them explicitly:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Lambda with no return value
If the interface returns void, just write the statements:
list.forEach(s -> System.out.println("Element: " + s));
Table: Syntax variations for lambda expressions
| What | Example | Comment |
|---|---|---|
| No parameters | |
For example, for Runnable |
| Single parameter | |
Parentheses can be omitted |
| Multiple parameters | |
Parentheses are required |
| Single expression | |
No return or braces |
| Block of code | |
Use return if there is a result |
3. Usage: where and how to use lambda expressions
Lambda expressions are most often used where you need to pass “behavior” — a function — as an argument. This was a real revolution for collections, streams (Stream API), events, and much more.
Sorting a list
Before Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
With a lambda expression:
list.sort((a, b) -> a.length() - b.length());
Creating a thread (Thread)
Thread t = new Thread(() -> System.out.println("Thread started!"));
t.start();
Processing collections
list.forEach(s -> System.out.println(s.toUpperCase()));
Filtering a list
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Example: Building out our learning app
Suppose we have a list of users:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Print all users whose names are longer than 4 characters:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("User: " + name));
4. Variable scope in lambda expressions
Lambda expressions can use variables from the enclosing method, but there are gotchas!
Variables and lambdas
A lambda in Java can capture only those variables that do not change after initialization. If a variable is declared final — that’s obvious. But even if the word final isn’t written, the compiler checks whether the value changes. If it doesn’t, it treats it as “effectively final” and happily allows it in the lambda.
Example:
int minLength = 4; // value never changes
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
This works because minLength stays the same number.
But if after using it in the lambda you try to reassign minLength, you’ll get a compilation error:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Error! The lambda has already "captured" the value
In essence, the rule is very simple: a variable used inside a lambda must be immutable.
Difference from anonymous classes
In anonymous classes and lambda expressions, variables from the outer method behave the same: only final/effectively final.
BUT!
For a lambda expression, this refers to the outer object (the current instance of the class), whereas for an anonymous class it refers to the instance of the anonymous class. This matters if you access fields or methods of the current class inside the lambda.
Example:
public class Example {
String name = "Outer class";
void demo() {
Runnable r1 = new Runnable() {
String name = "Anonymous class";
@Override
public void run() {
System.out.println(this.name); // "Anonymous class"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Outer class"
r1.run();
r2.run();
}
}
5. Common mistakes when working with lambda expressions
Error #1: Using a non-final/not effectively final variable. If you decide to change a variable after using it in a lambda, the compiler will stop you. This is for safety: otherwise it would be unclear which value to use.
Error #2: Confusion with this. In a lambda expression, this is the outer class, while in an anonymous class it is the anonymous class itself. If you try to call a method of the outer class from a lambda, everything will work, but from an anonymous class it won’t (if you were relying on the outer class context).
Error #3: Lambda without a context. A lambda expression cannot be used by itself — you must either assign it to a functional interface variable or pass it where that interface is expected. Trying to just write x -> x + 1 outside of a context will cause an error.
Error #4: An overly complex lambda. If a lambda grows beyond 3–5 lines, it’s hard to read. In such cases, move the logic to a separate method.
GO TO FULL VERSION