Lambda Expressions in Java
A lambda is a short way to write an implementation of a single method interface, so behaviour can be passed around like a value.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Definition
A lambda expression is an anonymous function written inline. It supplies the implementation of the one abstract method of a functional interface, which is what lets behaviour be passed to a method as an argument.
Before and after
// Java 7 and earlier
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Working");
}
};
// Java 8 onwards
Runnable task = () -> System.out.println("Working");Syntax
(parameters) -> expression
(parameters) -> { statements; }() -> 42 // no parameters
name -> name.length() // one parameter, brackets optional
(a, b) -> a + b // two parameters
(String a, String b) -> a.compareTo(b) // explicit types, rarely needed
(var a, var b) -> a + b // var form, Java 11 onwards
value -> { // a block body needs return
int doubled = value * 2;
return doubled + 1;
}The parameter types are inferred from the target type, which is why they are almost always omitted.
Where a lambda can be used
List<String> names = new ArrayList<>(List.of("Ravi", "Anita", "Meera"));
names.sort((a, b) -> a.compareTo(b)); // Comparator
names.forEach(name -> System.out.println(name)); // Consumer
names.removeIf(name -> name.startsWith("A")); // Predicate
Supplier<LocalDate> today = () -> LocalDate.now();
Function<String, Integer> length = text -> text.length();A lambda only works where the compiler knows the target type. That target must be a functional interface, an interface with exactly one abstract method.
// Object task = () -> System.out.println("x"); // compile error, no target type
Runnable task = () -> System.out.println("x"); // fineA practical example
public class NoteFilter {
public static List<Note> filter(List<Note> notes, Predicate<Note> condition) {
List<Note> result = new ArrayList<>();
for (Note note : notes) {
if (condition.test(note)) {
result.add(note);
}
}
return result;
}
}List<Note> published = NoteFilter.filter(notes, note -> note.isPublished());
List<Note> recent = NoteFilter.filter(notes, note -> note.created().isAfter(cutoff));
List<Note> both = NoteFilter.filter(notes,
note -> note.isPublished() && note.views() > 100);One method, any number of filtering rules. Before lambdas this required either a class per rule or a flag parameter that grew with every new case.
Variable capture
int threshold = 100; // effectively final
Predicate<Note> popular = note -> note.views() > threshold;
// threshold = 200; // would break the lambda aboveA lambda may use a local variable only if it is final or effectively final, meaning never reassigned. The value is captured, and allowing later reassignment would make the captured value ambiguous. Fields and static fields have no such restriction, because the lambda captures this rather than a copy.
// A common workaround that is usually a warning sign
int[] counter = {0};
names.forEach(name -> counter[0]++); // legal, but mutable shared state
long count = names.stream().filter(name -> name.length() > 4).count(); // betterthis inside a lambda
public class Service {
private final String name = "search";
public Runnable asLambda() {
return () -> System.out.println(this.name); // the Service instance
}
public Runnable asAnonymous() {
return new Runnable() {
@Override public void run() {
System.out.println(Service.this.name); // needs qualification
}
};
}
}A lambda does not create a new scope for this. It sees the enclosing instance directly, unlike an anonymous class. This is one of the practical reasons lambdas are easier to reason about.Lambda compared with anonymous class
| Aspect | Lambda | Anonymous class |
|---|---|---|
| Target | Functional interfaces only | Any interface or class |
| Abstract methods | Exactly one | Any number |
| Own fields | No | Yes |
this | The enclosing instance | The anonymous instance |
| Compiled to | An invokedynamic call site | A separate class file |
| Readability | Short | Verbose |
Common mistakes
- Assigning a lambda to a variable of a non functional type such as
Object. - Reassigning a captured local variable.
- Writing a lambda body of twenty lines. Extract a method and use a method reference.
- Forgetting
returnin a block bodied lambda. - Using an array or a field to accumulate state inside a lambda, instead of the operation designed for it.
- Catching checked exceptions awkwardly, because most built in functional interfaces do not declare any.
Checked exceptions in lambdas
// Does not compile: Function.apply declares no checked exception
// paths.stream().map(path -> Files.readString(path)).toList();
List<String> contents = paths.stream()
.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.toList();When the body grows like this, move it into a private method and reference it instead.
Best practices
- Keep lambdas to one expression where possible.
- Extract anything longer than about three lines into a named method.
- Prefer a method reference when the lambda only forwards its arguments.
- Give parameters meaningful names;
notereads better thann. - Keep lambdas free of side effects, especially inside streams.
- Omit parameter types and let inference work.
Practice
- Convert an anonymous
Comparatorinto a lambda and then into a method reference. - Why does the compiler reject
Object o = () -> {};? - Write a method taking a
Predicate<String>and call it with three different rules. - Explain the effectively final restriction with a short example that fails to compile.
- Show what
thisrefers to inside a lambda declared in an instance method.
Conclusion
A lambda is the implementation of a single method interface written where it is needed. It makes behaviour a value you can pass, and it is the foundation everything in the Stream API is built on.