- Understand what lambda expressions are and why they were introduced
- Learn the basic syntax of lambda expressions
- Understand functional interfaces and their relationship with lambdas
- Know the difference between lambdas and anonymous classes
Introduction to Lambda Expressions
Before Java 8, passing behavior to a method meant creating an anonymous class. Verbose. Intent obscured. Lambda expressions changed everything—treat functionality as a method argument, concise and expressive.
Think of lambdas as shorthand for implementing single-method interfaces.
The Problem Lambda Solves
Sorting a list with a custom comparator before Java 8:
// Before Java 8: Anonymous class (verbose!)
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
That's 6 lines of code for a simple comparison! Most of it is boilerplate. Now with lambda:
// Java 8+: Lambda expression (concise!)
Collections.sort(names, (a, b) -> a.compareTo(b));
One line, clear intent. The lambda (a, b) -> a.compareTo(b) captures the essence of what we're doing.
What is a Lambda Expression?
A lambda expression is an anonymous function - a block of code that you can pass around. It has:
- Parameters (can be zero or more)
- Arrow token (
->) - Body (expression or block of statements)
// Basic syntax
(parameters) -> expression
// Or with a block body
(parameters) -> { statements; }
Simple Examples
// No parameters
() -> System.out.println("Hello!")
// One parameter (parentheses optional)
x -> x * 2
(x) -> x * 2
// Two parameters
(a, b) -> a + b
// With explicit types
(String s) -> s.length()
// Block body with return
(int a, int b) -> {
int sum = a + b;
return sum;
}
Functional Interfaces
Lambda expressions work with functional interfaces - interfaces that have exactly one abstract method. The lambda provides the implementation for that method.
@FunctionalInterface
public interface Runnable {
void run(); // Single abstract method
}
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // Single abstract method
// (other methods are default or static)
}
The @FunctionalInterface annotation is optional but recommended - it ensures the interface has only one abstract method.
Creating Your Own Functional Interface
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
}
// Using it with lambdas
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
Calculator max = (a, b) -> Math.max(a, b);
System.out.println(add.calculate(5, 3)); // 8
System.out.println(multiply.calculate(5, 3)); // 15
System.out.println(max.calculate(5, 3)); // 5
Why Lambda Expressions Matter
1. Conciseness
// Without lambda: 4 lines
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked!");
}
});
// With lambda: 1 line
button.addActionListener(e -> System.out.println("Clicked!"));
2. Readability
The code expresses intent more directly:
// What are we doing? Filtering, mapping, collecting
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
3. Enables Functional Programming
Lambdas enable powerful functional programming patterns:
// Function composition
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> double_ = x -> x * 2;
Function<Integer, Integer> addOneThenDouble = addOne.andThen(double_);
System.out.println(addOneThenDouble.apply(5)); // 12
4. Better API Design
APIs can now accept behavior as parameters:
public void processData(List<String> data, Predicate<String> filter) {
for (String item : data) {
if (filter.test(item)) {
System.out.println(item);
}
}
}
// Caller decides the filtering logic
processData(names, name -> name.length() > 5);
processData(names, name -> name.startsWith("J"));
Lambda vs Anonymous Class
| Aspect | Lambda | Anonymous Class |
|---|---|---|
| Syntax | Concise | Verbose |
this keyword |
Refers to enclosing class | Refers to anonymous class |
| Scope | Captures effectively final variables | Can have its own state |
| Interfaces | Only functional interfaces | Any interface/class |
| Performance | Slightly better (no class file) | Creates a class file |
The this Difference
public class Example {
private String name = "Outer";
public void demonstrate() {
// Anonymous class: 'this' refers to the anonymous class
Runnable anon = new Runnable() {
private String name = "Inner";
public void run() {
System.out.println(this.name); // "Inner"
}
};
// Lambda: 'this' refers to the enclosing class
Runnable lambda = () -> {
System.out.println(this.name); // "Outer"
};
}
}
Variable Capture
Lambdas can access variables from their enclosing scope, but those variables must be effectively final (never modified after initialization):
String prefix = "Hello, "; // Effectively final
// OK: prefix is never modified
Consumer<String> greeter = name -> System.out.println(prefix + name);
// NOT OK: can't modify captured variable
// prefix = "Hi, "; // Compilation error!
Why this restriction? Lambdas may execute on different threads, and modifying shared variables would cause concurrency issues.
Common Use Cases
Event Handling
button.setOnAction(event -> handleClick());
Collection Operations
list.forEach(item -> System.out.println(item));
list.removeIf(item -> item.isEmpty());
list.sort((a, b) -> a.compareTo(b));
Thread Creation
new Thread(() -> {
System.out.println("Running in thread");
}).start();
Stream Operations
numbers.stream()
.filter(n -> n > 0)
.map(n -> n * 2)
.forEach(n -> System.out.println(n));
Lambdas revolutionized Java: (params) -> body is anonymous function syntax. Functional interfaces have exactly one abstract method. Use @FunctionalInterface to document them. Lambdas can access effectively final variables. this refers to the enclosing class, not the lambda. They enable functional programming but only work with functional interfaces—not a replacement for all anonymous classes.
