LearnFrenzy
Sign in Take a free test

Generics, Lambdas and Streams · Lesson 35 of 42

Lambdas, Functional Interfaces and Method References

5 min read Updated Nov 7, 2026 Saurabh Samir

Java 8 added lambda expressions: short, unnamed functions you can pass around like data. They replaced pages of anonymous classes, made collection code far shorter, and unlocked the Stream API. Modern Java – and every Spring Boot codebase – uses them everywhere.

In this lesson

  • Lambda syntax and where lambdas can be used
  • The core functional interfaces in java.util.function
  • Method references
  • Optional for values that may be missing

From anonymous class to lambda

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Chitra", "Om", "Bela"));

        // Before Java 8: an anonymous class
        names.sort(new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        });
        System.out.println(names);

        // With a lambda: same meaning, one line
        names.sort((a, b) -> b.compareTo(a));
        System.out.println(names);
    }
}

Output

[Om, Bela, Chitra]
[Om, Chitra, Bela]

Lambda syntax

  1. (a, b)parameters – types are inferred
  2. ->arrow
  3. b.compareTo(a)body: one expression (returned automatically) or { statements; return …; }

A lambda can be used wherever a functional interface is expected – an interface with exactly one abstract method, like Comparator, Runnable or your own PriceRule from chapter 10.

The core functional interfaces

InterfaceMethodShapeTypical use
Predicate<T>test(T)T → booleanfilters, validation
Function<T,R>apply(T)T → Rconversions, mapping
Consumer<T>accept(T)T → nothingprinting, saving
Supplier<T>get()nothing → Tlazy values, factories
BiFunction<T,U,R>apply(T, U)(T, U) → Rcombining two values
UnaryOperator<T>apply(T)T → Ttransformations
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Predicate<String> isEmail = s -> s.contains("@") && s.contains(".");
        Predicate<String> isShort = s -> s.length() < 6;
        System.out.println(isEmail.test("asha@mail.com") + " " + isEmail.and(isShort.negate()).test("a@b.c"));

        Function<Double, Double> addGst = p -> p * 1.18;
        Function<Double, Long> round = Math::round;
        System.out.println(addGst.andThen(round).apply(999.0));

        Consumer<String> greet = n -> System.out.println("Hello, " + n);
        greet.accept("Kabir");

        Supplier<String> otp = () -> "OTP-" + 482913;
        System.out.println(otp.get());

        BiFunction<Integer, Integer, Integer> power = (b, e) -> (int) Math.pow(b, e);
        System.out.println(power.apply(2, 10));
    }
}

Output

true false
1179
Hello, Kabir
OTP-482913
1024

Predicates combine with and, or, negate; functions chain with andThen and compose.

Variables captured by a lambda

import java.util.List;

public class Main {
    public static void main(String[] args) {
        double discount = 0.1;             // effectively final: never reassigned
        List.of(100.0, 250.0).forEach(p -> System.out.println(p * (1 - discount)));
        // discount = 0.2;   → error: variables used in a lambda must be effectively final
    }
}

Output

90.0
225.0

Method references

When a lambda only calls an existing method, a method reference says the same thing more briefly:

KindLambdaMethod reference
static methods -> Integer.parseInt(s)Integer::parseInt
instance method of the arguments -> s.toUpperCase()String::toUpperCase
method of a specific objectx -> System.out.println(x)System.out::println
constructor() -> new ArrayList<>()ArrayList::new
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> raw = List.of("12", "7", "30");
        List<Integer> nums = new ArrayList<>();
        raw.stream().map(Integer::parseInt).forEach(nums::add);
        System.out.println(nums);
        List.of("java", "sql").stream().map(String::toUpperCase).forEach(System.out::println);
    }
}

Output

[12, 7, 30]
JAVA
SQL

Optional: no more surprise nulls

Optional<T> is a box that either contains a value or is empty. Returning it from a method makes "this might not exist" visible in the type, instead of returning null and hoping callers check.

import java.util.Map;
import java.util.Optional;

public class Main {
    static final Map<Integer, String> USERS = Map.of(1, "asha", 2, "ben");

    static Optional<String> findUser(int id) {
        return Optional.ofNullable(USERS.get(id));
    }

    public static void main(String[] args) {
        System.out.println(findUser(1).map(String::toUpperCase).orElse("unknown"));
        System.out.println(findUser(9).map(String::toUpperCase).orElse("unknown"));
        findUser(2).ifPresent(u -> System.out.println("Welcome back, " + u));
        System.out.println(findUser(9).isPresent());
        try {
            findUser(9).orElseThrow(() -> new IllegalArgumentException("No user 9"));
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

Output

ASHA
unknown
Welcome back, ben
false
No user 9

Common mistake: Optional.get() without checking

optional.get() on an empty Optional throws NoSuchElementException – just a different crash. Use orElse, orElseGet, orElseThrow, map or ifPresent. Use Optional for return types, not for fields or parameters.

Practice

Try it yourself

  1. Create a Predicate<Integer> for "even and greater than 10" and test 12, 7 and 8.

    Show solution
    import java.util.function.Predicate;
    
    public class Main {
        public static void main(String[] args) {
            Predicate<Integer> even = n -> n % 2 == 0;
            Predicate<Integer> big = n -> n > 10;
            Predicate<Integer> both = even.and(big);
            for (int n : new int[]{12, 7, 8}) System.out.println(n + " " + both.test(n));
        }
    }

    Output

    12 true
    7 false
    8 false

Quick check

What makes an interface "functional"?

It has exactly one abstract method.

Which functional interface takes nothing and returns a value?

Supplier<T>

Rewrite s -> s.length() as a method reference.

String::length

Summary

  • Lambdas implement functional interfaces: (params) -> body.
  • Know Predicate, Function, Consumer, Supplier, BiFunction; compose with and/or/andThen.
  • Method references shorten simple lambdas; Optional makes missing values explicit.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular