Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,051visitors

Built for engineers who want to go deep.


← Java Streams & Functional Programming

Lambdas & Functional Interfaces

  • Lambda Expressions
  • Method References

Streams API

  • Streams API
  • Collectors and groupingBy
  • Optional
Chaturmind
← Java Streams & Functional Programming

Lambdas & Functional Interfaces

  • Lambda Expressions
  • Method References

Streams API

  • Streams API
  • Collectors and groupingBy
  • Optional
HomeLearnJavaJava Streams & Functional ProgrammingLambdas & Functional Interfaces
✓ FreeIntermediate· 6 min read

Lambda Expressions

Replace anonymous classes with concise lambda syntax — the shift to functional Java.

Published September 21, 2026


Lambda Expressions

A lambda expression (Java 8+) is a short way to write a piece of behaviour as a value, a function you can store in a variable, pass to a method, or return. It replaces the bulky anonymous classes that Java previously needed whenever code had to be passed around: comparators, event handlers, tasks for a thread pool, filters for a stream.

// Before: an anonymous class just to say "compare by length"
Collections.sort(words, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});

// With a lambda
words.sort((a, b) -> Integer.compare(a.length(), b.length()));

// With a ready-made comparator factory and a method reference
words.sort(Comparator.comparingInt(String::length));

Advertisement

Syntax

() -> System.out.println("hi")                 // no parameters
x -> x * 2                                     // one parameter: parentheses optional
(a, b) -> a + b                                // several parameters
(String s, int n) -> s.repeat(n)               // explicit types (normally inferred)
(a, b) -> {                                    // block body: statements, explicit return
    int sum = a + b;
    return sum * sum;
}

A single-expression body returns its value automatically. A block body needs return (unless the result type is void).

Where lambdas can go: functional interfaces

A lambda has no type of its own. It takes the type of the functional interface expected where it's used: an interface with exactly one abstract method. The lambda is the implementation of that method, and its parameter and return types must match. This is called target typing.

Runnable task = () -> System.out.println("running");          // void run()
Comparator<String> byLength = (a, b) -> a.length() - b.length();   // int compare(T, T)

The java.util.function package provides the common shapes, so you rarely need to declare your own:

InterfaceMethodShapeTypical use
Supplier<T>T get()() → TLazily create a value (orElseGet)
Consumer<T>void accept(T)T → nothingAct on a value (forEach)
Function<T,R>R apply(T)T → RTransform (map)
Predicate<T>boolean test(T)T → booleanFilter (filter, removeIf)
BiFunction<T,U,R>R apply(T,U)(T, U) → RCombine two values
UnaryOperator<T>T apply(T)T → TSame-type transform (replaceAll)
BinaryOperator<T>T apply(T,T)(T, T) → TReduce (reduce, merge)

There are primitive specializations too (IntPredicate, ToIntFunction, IntUnaryOperator, …). They avoid boxing int into Integer, which matters in hot code.

These interfaces also have helpful default methods for composing behaviour:

Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> shortWord = s -> s.length() < 5;
Predicate<String> valid = nonEmpty.and(shortWord.negate());

Function<String, String> trim = String::strip;
Function<String, Integer> trimmedLength = trim.andThen(String::length);

Comparator<Person> order = Comparator.comparing(Person::lastName).thenComparing(Person::firstName).reversed();

Capturing variables: "effectively final"

A lambda can use local variables from its surrounding method, but only if they're effectively final, meaning never reassigned after initialization:

String greeting = "Hello, ";
names.forEach(n -> System.out.println(greeting + n));    // ✅ greeting never changes

int count = 0;
names.forEach(n -> count++);                              // ❌ compile error: count is modified

Why? A lambda can outlive the method call that created it (it may run later, or on another thread), so it captures the variable's value, not the variable itself. Allowing reassignment would make the captured copy and the original silently diverge, and would invite data races. Fields and array elements can be modified from a lambda, which is exactly why doing so from parallel code is dangerous. To aggregate, use the stream's own operations (count(), sum(), collectors) instead of mutating outside variables.

Lambdas vs anonymous classes

They look interchangeable, but there are real differences:

  • this: inside a lambda, this means the enclosing object. Inside an anonymous class, it means the anonymous class instance.
  • Scope: a lambda doesn't introduce a new variable scope, so it can't declare a parameter with the same name as a local variable. An anonymous class can shadow names.
  • State: an anonymous class can have its own fields and several methods. A lambda is only a single function.
  • Implementation: lambdas compile to an invokedynamic call, with no extra .class file per lambda, and the JVM can reuse non-capturing lambda instances. They're generally at least as efficient.

Checked exceptions: the awkward part

Standard functional interfaces don't declare checked exceptions, so a lambda body that calls something throwing IOException won't compile as a Function:

paths.stream().map(p -> Files.readString(p));      // ❌ unhandled IOException

paths.stream().map(p -> {                          // ✅ handle or wrap it
    try { return Files.readString(p); }
    catch (IOException e) { throw new UncheckedIOException(e); }
});

For repeated cases, a small helper that wraps a throwing function, or a custom functional interface that declares the exception, keeps pipelines readable.

Writing good lambdas

  • Keep them short. One expression, or a few lines. If a lambda needs a comment, extract a named private method and pass a method reference (this::isEligible). The call site then reads like a sentence.
  • Avoid side effects in stream lambdas (modifying collections or fields). Let the stream produce the result.
  • Prefer the standard functional interfaces over declaring new ones, unless a domain name (DiscountPolicy) makes the code clearer.
  • Don't use lambdas where they hide control flow. A plain loop with an early return or break is sometimes clearer than a stream pipeline.

Follow-up questions this topic invites — and their answers

Q: What is a functional interface? A: An interface with exactly one abstract method. Default and static methods don't count. It's the only kind of type a lambda or method reference can be converted to. @FunctionalInterface asks the compiler to enforce the rule.

Q: Why must captured local variables be effectively final? A: The lambda captures a copy of the variable's value, and it may run after the method has returned or on another thread. Permitting reassignment would make that copy silently stale and invite race conditions, so the language forbids it.

Q: What does this refer to inside a lambda? A: The instance of the enclosing class, the same as in the surrounding code. In an anonymous inner class, this refers to the anonymous object itself. That's one of the practical differences between the two.

Q: Are lambdas slower than anonymous classes? A: No. They're implemented with invokedynamic, avoiding a separate class file per lambda, and non-capturing lambdas can be cached as single instances. Any performance differences are negligible compared with the work the lambda does. Boxing (using Function<Integer, Integer> instead of IntUnaryOperator) is a more likely cost.

Q: How do you use a method that throws a checked exception inside a lambda? A: Catch it inside the lambda and handle it or rethrow it as an unchecked exception (UncheckedIOException), or define a functional interface whose method declares the exception. Standard interfaces like Function don't allow checked exceptions.

Advertisement

Next

Method References

AI Tutor

Lesson: Lambda Expressions

Quick actions

AI responses can be inaccurate. Verify critical information.