It is used to implement functional interfaces (interfaces with exactly one abstract method).
Lambda expressions were introduced in Java 8 to make code more concise, especially for writing inline implementations of interfaces.
How Lambda Expression is Mapped to a Functional Interface Method ?
- Each lambda expression is mapped to the single abstract method (SAM) of its target functional interface.
- In other words, the lambda body becomes the implementation of that method.
- Examples:
- Runnable → run() : () -> System.out.println("Run")
- Consumer<T> → accept(T t) : x -> System.out.println(x)
- Supplier<T> → get() : () -> Math.random()
- Predicate<T> → test(T t) : n -> n > 0
- Function<T,R> → apply(T t) : s -> s.length()
- Comparator<T> → compare(T a, T b) : (a, b) -> a.compareTo(b)
- UnaryOperator<T> → apply(T t) : s -> s.toUpperCase()
- BinaryOperator<T> → apply(T a, T b) : (a, b) -> a + b
-
parameter_list → input parameters (can be empty or multiple).
-
-> → lambda operator (arrow token).
-
body_of_expression → the code block (can be a single statement or multiple statements inside
{}
).
-
No Parameters
-
Single Parameter
-
Multiple Parameters
-
With Block Body
π Functional interface: Runnable
π Abstract method: run()
π Functional interface: Comparator<T>
π Abstract method: compare(T a, T b)
π Functional interface: Consumer<T>
π Abstract method: accept(T t)
π Internally executes:
Java lambdas are not anonymous inner classes under the hood.
Instead, the JVM uses a special instruction called invokedynamic.
Step 1: Source Code (Example)
Step 2: What Happens During Compilation
-
No new
.class
file for the lambda. -
The lambda body becomes a private synthetic method:
Step 3: invokedynamic Instruction
The JVM links at runtime using:
Internal Example with Parameters
Key Difference vs Anonymous Inner Classes
Aspect | Anonymous Inner Class | Lambda Expression |
Class File | Creates new .class file | No new .class file |
Runtime | Separate class instance | Uses invokedynamic + LambdaMetafactory |
Performance | Slightly heavier | Lightweight & optimized |
Readability | Verbose | Concise |
invokedynamic
.πΉ Practical Examples & Use Cases of Lambdas
1. Simplifying Runnable
π Functional interface: Runnable
π Method called: run()
2. Sorting with Comparator
π Functional interface: Comparator<T>
π Method called: compare(T a, T b)
3. Iterating Collections (forEach)
π Functional interface: Consumer<T>
π Method called: accept(T t)
4. Using Lambdas with Streams
5. Custom Functional Interface
6. Map Iteration
7. Removing Elements Easily
8. Replacing Elements
- Single Abstract Method (SAM) Rule → Every lambda expression implements the one abstract method of the functional interface.
- Common Functional Interfaces and Their Methods:
Runnable
→run()
Comparator
→compare()
Predicate
→test()
Function
→apply()
Consumer
→accept()
Supplier
→get()
- Default or Static Methods → Any additional default or static methods in the interface are ignored; only the abstract method is implemented.
- Runtime Behavior → The JVM, using
invokedynamic
andLambdaMetafactory
, dynamically creates an instance of the functional interface, linking the lambda body to its abstract method.
πΉ Java Functional Interfaces & Real-World Use Cases
Functional Interface | Method | Lambda Form | Common Use Cases | Example |
Runnable | void run() | () -> {} | Multi-threading, tasks | new Thread(() -> System.out.println("Run")).start(); |
Comparator<T> | int compare(T a, T b) | (a, b) -> int | Sorting | list.sort((a, b) -> a.length() - b.length()); |
Consumer<T> | void accept(T t) | x -> {} | Iteration, event handling | list.forEach(s -> System.out.println(s)); |
Supplier<T> | T get() | () -> value | Generating values | Supplier<Double> rand = () -> Math.random(); |
Predicate<T> | boolean test(T t) | x -> boolean | Filtering | list.removeIf(n -> n < 0); |
Function<T,R> | R apply(T t) | x -> result | Mapping | list.stream().map(s -> s.length()); |
BiConsumer<T,U> | void accept(T,U) | (x,y) -> {} | Map iteration | map.forEach((k,v) -> System.out.println(k+"="+v)); |
BiFunction<T,U,R> | R apply(T,U) | (x,y) -> result | Merge/combine | BiFunction<Integer,Integer,Integer> add = (a,b)->a+b; |
UnaryOperator<T> | T apply(T t) | x -> x | Replace/update | list.replaceAll(s -> s.toUpperCase()); |
BinaryOperator<T> | T apply(T,T) | (a,b) -> result | Reduction | list.stream().reduce((a,b)->a+b); |
Note: Each lambda expression is always mapped to the single abstract method (SAM) of the corresponding functional interface.
In short: specific lambda → specific functional interface method.
Example mappings to remember:
- () -> { … } → Runnable.run()
- x -> { … } → Consumer.accept(T t)
- (a, b) -> … → Comparator.compare(T a, T b)
- x -> boolean → Predicate.test(T t)
- x -> result → Function.apply(T t)
- (a, b) -> result → BinaryOperator.apply(T a, T b)
Tip: Memorizing these helps you quickly identify which functional interface and method a lambda corresponds to.
Lambda Use Cases (Expanded)
- Lambdas in Java always implement the single abstract method of a functional interface.
-
The compiler + JVM (
invokedynamic
+LambdaMetafactory
) ensures they are lightweight, efficient, and expressive.
Stream Basics
A Stream represents a sequence of elements supporting functional-style operations.
- Intermediate operations → return another Stream (lazy).
- Terminal operations → produce a result or side-effect (eager).
Lifecycle:
Collection → Stream → Intermediate Ops → Terminal Op → Result
πΉ Intermediate Operations
Stream Method | Functional Interface | SAM (Method) | Lambda Example |
filter(Predicate<T>) | Predicate<T> | test(T t) | n -> n % 2 == 0 |
map(Function<T,R>) | Function<T,R> | apply(T t) | s -> s.length() |
flatMap(Function<T,Stream<R>>) | Function<T, Stream<R>> | apply(T t) | s -> Arrays.stream(s.split("")) |
distinct() | — | — | — |
sorted() | Comparator<T> | compare(T a, T b) | (a, b) -> a.compareTo(b) |
sorted(Comparator<T>) | Comparator<T> | compare(T a, T b) | (a, b) -> b - a |
peek(Consumer<T>) | Consumer<T> | accept(T t) | x -> System.out.println(x) |
limit(long n) | — | — | — |
skip(long n) | — | — | — |
πΉ Terminal Operations
Stream Method | Functional Interface | SAM (Method) | Lambda Example |
forEach(Consumer<T>) | Consumer<T> | accept(T t) | x -> System.out.println(x) |
toArray() | — | — | — |
reduce(BinaryOperator<T>) | BinaryOperator<T> | apply(T a, T b) | (a, b) -> a + b |
reduce(T, BinaryOperator<T>) | BinaryOperator<T> | apply(T a, T b) | (a, b) -> a * b |
collect(Collector) | Collector (not lambda) | — | Collectors.toList() |
min(Comparator<T>) | Comparator<T> | compare(T a, T b) | (a, b) -> a.compareTo(b) |
max(Comparator<T>) | Comparator<T> | compare(T a, T b) | (a, b) -> a.compareTo(b) |
count() | — | — | — |
anyMatch(Predicate<T>) | Predicate<T> | test(T t) | x -> x > 10 |
allMatch(Predicate<T>) | Predicate<T> | test(T t) | x -> x % 2 == 0 |
noneMatch(Predicate<T>) | Predicate<T> | test(T t) | x -> x < 0 |
findFirst() | — | — | — |
findAny() | — | — | — |
πΉ Full Examples of Stream API
π This example shows how we use Lambda expressions (via Functional Interfaces) with the Stream API.
π It also explains which intermediate/terminal method maps to which functional interface.
import java.util.*;
import java.util.stream.*;
public class StreamAPIExamples {
public static void main(String[] args) {
List<String> words = Arrays.asList("java", "lambda", "stream", "java", "code");
// 1. filter(Predicate<T>) → Predicate.test(T t)
List<String> filtered = words.stream()
.filter(w -> w.length() > 4) // keep words longer than 4 chars
.toList();
System.out.println("Filter (>4): " + filtered); // [lambda, stream]
// 2. map(Function<T,R>) → Function.apply(T t)
List<Integer> lengths = words.stream()
.map(s -> s.length())
.toList();
System.out.println("Map (lengths): " + lengths); // [4, 6, 6, 4, 4]
// 3. flatMap(Function<T,Stream<R>>) → Function.apply(T t)
List<String> chars = words.stream()
.flatMap(s -> Arrays.stream(s.split(""))) // break into characters
.toList();
System.out.println("FlatMap (chars): " + chars);
// 4. distinct()
List<String> distinct = words.stream()
.distinct()
.toList();
System.out.println("Distinct: " + distinct); // [java, lambda, stream, code]
// 5. sorted(Comparator<T>) → Comparator.compare(T a, T b)
List<String> sorted = words.stream()
.sorted((a, b) -> a.compareTo(b))
.toList();
System.out.println("Sorted: " + sorted);
// 6. peek(Consumer<T>) → Consumer.accept(T t)
List<String> peeked = words.stream()
.peek(s -> System.out.println("Peek: " + s))
.toList();
System.out.println("Peeked list: " + peeked);
// 7. limit() + skip()
List<String> limited = words.stream().skip(2).limit(2).toList();
System.out.println("Skip 2 + Limit 2: " + limited); // [stream, java]
// ========== TERMINAL OPERATIONS ==========
List<Integer> nums = Arrays.asList(5, 3, 8, 2, 7, 8);
// 8. forEach(Consumer<T>) → Consumer.accept(T t)
System.out.print("ForEach: ");
nums.forEach(n -> System.out.print(n + " "));
System.out.println();
// 9. toArray()
Object[] arr = nums.stream().toArray();
System.out.println("ToArray: " + Arrays.toString(arr));
// 10. reduce(BinaryOperator<T>) → BinaryOperator.apply(T a, T b)
int sum = nums.stream().reduce((a, b) -> a + b).orElse(0);
System.out.println("Reduce (sum): " + sum);
// 11. collect(Collectors.toSet())
Set<Integer> set = nums.stream().collect(Collectors.toSet());
System.out.println("Collect toSet: " + set);
// 12. min(Comparator<T>) → Comparator.compare(T a, T b)
int min = nums.stream().min((a, b) -> a - b).get();
System.out.println("Min: " + min);
// 13. max(Comparator<T>)
int max = nums.stream().max((a, b) -> a - b).get();
System.out.println("Max: " + max);
// 14. count()
long count = nums.stream().count();
System.out.println("Count: " + count);
// 15. anyMatch(Predicate<T>) → Predicate.test(T t)
boolean anyEven = nums.stream().anyMatch(n -> n % 2 == 0);
System.out.println("Any even? " + anyEven);
// 16. allMatch(Predicate<T>)
boolean allPositive = nums.stream().allMatch(n -> n > 0);
System.out.println("All positive? " + allPositive);
// 17. noneMatch(Predicate<T>)
boolean noneNegative = nums.stream().noneMatch(n -> n < 0);
System.out.println("None negative? " + noneNegative);
// 18. findFirst()
int first = nums.stream().findFirst().get();
System.out.println("FindFirst: " + first);
// 19. findAny()
int any = nums.stream().findAny().get();
System.out.println("FindAny: " + any);
}
}
Output (approximate, may vary on distinct/sorted order)
Filter (>4): [lambda, stream]
Map (lengths): [4, 6, 6, 4, 4]
FlatMap (chars): [j, a, v, a, l, a, m, b, d, a, s, t, r, e, a, m, j, a, v, a, c, o, d, e]
Distinct: [java, lambda, stream, code]
Sorted: [code, java, java, lambda, stream]
Peek: java
Peek: lambda
Peek: stream
Peek: java
Peek: code
Peeked list: [java, lambda, stream, java, code]
Skip 2 + Limit 2: [stream, java]
ForEach: 5 3 8 2 7 8
ToArray: [5, 3, 8, 2, 7, 8]
Reduce (sum): 33
Collect toSet: [2, 3, 5, 7, 8]
Min: 2
Max: 8
Count: 6
Any even? true
All positive? true
None negative? true
FindFirst: 5
FindAny: 5
Summary
- Every Stream method expecting a lambda maps to one functional interface SAM.
- Intermediate methods:
- filter → Predicate.test
- map → Function.apply
- peek → Consumer.accept
- sorted → Comparator.compare
- Terminal methods:
- forEach → Consumer.accept
- reduce → BinaryOperator.apply
- anyMatch → Predicate.test
- Other methods like distinct, limit, skip, count don’t require lambdas.