Lambda Expressions



πŸ”Ή Definition

lambda expression in Java is essentially a short block of code that takes in parameters and returns a value.


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

πŸ”Ή Syntax
(parameter_list) -> { body_of_expression }
  • 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 {}).


Example 1: Without Lambda
// Functional interface @FunctionalInterface interface MyFunctionalInterface { void sayHello(); } public class LambdaExample { public static void main(String[] args) { // Using anonymous class MyFunctionalInterface obj = new MyFunctionalInterface() { public void sayHello() { System.out.println("Hello World!"); } }; obj.sayHello(); } }

Example 2: With Lambda
@FunctionalInterface interface MyFunctionalInterface { void sayHello(); } public class LambdaExample { public static void main(String[] args) { // Using Lambda expression MyFunctionalInterface obj = () -> System.out.println("Hello World with Lambda!"); obj.sayHello(); } }

πŸ”Ή Types of Lambda Expressions
  1. No Parameters

    () -> System.out.println("Hello Lambda");
  2. Single Parameter

    x -> x * x // takes one parameter and returns square
  3. Multiple Parameters

    (a, b) -> a + b
  4. With Block Body

    (a, b) -> { System.out.println("Adding numbers..."); return a + b; }

Example 3: Lambda with Runnable
public class RunnableLambda { public static void main(String[] args) { // Without Lambda Runnable r1 = new Runnable() { public void run() { System.out.println("Thread 1 running"); } }; new Thread(r1).start(); // With Lambda Runnable r2 = () -> System.out.println("Thread 2 running"); new Thread(r2).start(); } }

πŸ‘‰ Functional interface: Runnable
πŸ‘‰ Abstract method: run()


Example 4: Lambda with Comparator
import java.util.*; public class ComparatorLambda { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob"); // Without Lambda Collections.sort(names, new Comparator<String>() { public int compare(String a, String b) { return a.compareTo(b); } }); // With Lambda Collections.sort(names, (a, b) -> a.compareTo(b)); System.out.println(names); // [Alice, Bob, John] } }

πŸ‘‰ Functional interface: Comparator<T>
πŸ‘‰ Abstract method: compare(T a, T b)


Example 5: ForEach with Lambda
import java.util.*; public class ForEachLambda { public static void main(String[] args) { List<String> list = Arrays.asList("Java", "Python", "C++"); // Using forEach with Lambda list.forEach(item -> System.out.println(item)); } }

πŸ‘‰ Functional interface: Consumer<T>
πŸ‘‰ Abstract method: accept(T t)
πŸ‘‰ Internally executes:

consumer.accept("Java"); consumer.accept("Python"); consumer.accept("C++");

πŸ”Ή How Lambda Works Internally in Java 

Java lambdas are not anonymous inner classes under the hood.
Instead, the JVM uses a special instruction called invokedynamic.

Step 1: Source Code (Example)

@FunctionalInterface interface MyInterface { void sayHello(); } public class LambdaInternals { public static void main(String[] args) { MyInterface obj = () -> System.out.println("Hello Lambda!"); obj.sayHello(); } }

Step 2: What Happens During Compilation

  • No new .class file for the lambda.

  • The lambda body becomes a private synthetic method:

private static void lambda$main$0() { System.out.println("Hello Lambda!"); }

Step 3: invokedynamic Instruction
The JVM links at runtime using:

LambdaMetafactory.metafactory()


Internal Example with Parameters

interface Calculator { int add(int a, int b); } public class LambdaInternals2 { public static void main(String[] args) { Calculator calc = (a, b) -> a + b; System.out.println(calc.add(5, 10)); } } // Internally private static int lambda$main$0(int a, int b) { return a + b; }

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


In short: Lambdas are compiled to private synthetic methods and linked at runtime with invokedynamic.

πŸ”Ή Practical Examples & Use Cases of Lambdas

1. Simplifying Runnable

Runnable r2 = () -> System.out.println("Thread running with Lambda..."); new Thread(r2).start();

πŸ‘‰ Functional interface: Runnable
πŸ‘‰ Method called: run()


2. Sorting with Comparator

Collections.sort(names, (a, b) -> a.compareTo(b));

πŸ‘‰ Functional interface: Comparator<T>
πŸ‘‰ Method called: compare(T a, T b)


3. Iterating Collections (forEach)

langs.forEach(lang -> System.out.println("Language: " + lang));

πŸ‘‰ Functional interface: Consumer<T>
πŸ‘‰ Method called: accept(T t)


4. Using Lambdas with Streams

numbers.stream() .filter(n -> n % 2 == 0) // Predicate.test .forEach(n -> System.out.println("Even: " + n)); // Consumer.accept
πŸ‘‰ Functional interface: Predicate<T> πŸ‘‰ Method called: test(T t) πŸ‘‰ Functional interface: Consumer<T> πŸ‘‰ Method called: accept(T t)

5. Custom Functional Interface

@FunctionalInterface interface Calculator { int operate(int a, int b); } Calculator add = (a, b) -> a + b; System.out.println(add.operate(5, 3)); // 8
πŸ‘‰ Functional interface: Calculator (custom) πŸ‘‰ Method called: operate(int a, int b)

6. Map Iteration

map.forEach((key, value) -> System.out.println(key + " -> " + value));
πŸ‘‰ Functional interface: BiConsumer<K,V> πŸ‘‰ Method called: accept(K key, V value)

7. Removing Elements Easily

nums.removeIf(n -> n % 2 == 0);
πŸ‘‰ Functional interface: Predicate<T> πŸ‘‰ Method called: test(T t)

8. Replacing Elements

list.replaceAll(s -> s.toUpperCase());
πŸ‘‰ Functional interface: UnaryOperator<T> πŸ‘‰ Method called: apply(T t)

πŸ”Ή Key Points
  • Single Abstract Method (SAM) Rule → Every lambda expression implements the one abstract method of the functional interface.
  • Common Functional Interfaces and Their Methods:
    • Runnablerun()
    • Comparatorcompare()
    • Predicatetest()
    • Functionapply()
    • Consumeraccept()
    • Supplierget()
  • 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 and LambdaMetafactory, 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 & Functional Interfaces
+-----------------------+ | Functional Interfaces | +-----------------------+ | -------------------------------------------------------- | | | | | Runnable Comparator Consumer Function Predicate Supplier (no args) (compare 2) (accept T) (T -> R) (T -> boolean) (no args -> T)
Examples
  • Runnable r = () -> System.out.println("Running...");
  • Comparator<String> comp = (a, b) -> a.compareTo(b);
  • Consumer<String> c = s -> System.out.println("Hello " + s);
  • Function<String, Integer> f = s -> s.length();
  • Predicate<Integer> isEven = n -> n % 2 == 0;
  • Supplier<Double> rand = () -> Math.random();

Lambda Use Cases (Expanded)

+--------------------------+ | Functional Interfaces | +--------------------------+ | -------------------------------------------------------- | | | | | Runnable Comparator Consumer Function Predicate Supplier (no args) (compare 2) (accept T) (T -> R) (T -> boolean) (no args -> T) | | | | | | v v v v v v ( ) -> {...} (a,b)->{...} x -> {...} x -> {...} x -> {...} () -> {...}

πŸ”Ή Real-Life Usage Example
import java.util.*; import java.util.function.*; public class LambdaCheat { public static void main(String[] args) { Runnable r = () -> System.out.println("Task running..."); Comparator<Integer> comp = (a, b) -> a - b; Consumer<String> c = s -> System.out.println("Hello " + s); Function<String, Integer> f = s -> s.length(); Predicate<Integer> p = n -> n > 0; Supplier<Double> s = () -> Math.random(); r.run(); System.out.println("Compare: " + comp.compare(5, 3)); c.accept("Lambda"); System.out.println("Length: " + f.apply("Java")); System.out.println("Positive? " + p.test(-1)); System.out.println("Random: " + s.get()); } }

πŸ”Ή Final Takeaway:
  • 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.






How we use Lambda Expressions (Functional Interfaces) in Stream API


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.