Lambda expressions (Java 8+) let you pass behaviour as a value: s -> s.isEmpty() instead of an anonymous class. They work with functional interfaces, interfaces with a single abstract method, and they're everywhere in modern Java and test code: streams, waits (wait.until(d -> …)), listeners and comparators. This guide also covers Optional.

Lambda Expressions & Functional Interfaces

In Simple Terms

Lambda = Mini anonymous function:

Instead of writing a whole class just to say 'hey, sort by name', you write a short arrow: (a, b) -> a.name.compareTo(b.name). It's like a sticky note with instructions, rather than a whole contract document.

Lambda Syntax

// Syntax: (parameters) -> expression/block

// No params
Runnable r = () -> System.out.println("Hello!");

// One param (parentheses optional)
Consumer<String> print = s -> System.out.println(s);

// Multiple params
Comparator<Integer> comp = (a, b) -> a - b;

// Block body (multiple statements)
BinaryOperator<Integer> add = (a, b) -> {
    int sum = a + b;
    return sum;
};

// ── BUILT-IN FUNCTIONAL INTERFACES ──
// Predicate<T>    — T → boolean
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
Predicate<Integer> isEvenOrNeg = isEven.or(isPositive.negate());

// Function<T, R>  — T → R
Function<String, Integer> length = String::length;
Function<Integer, Integer> doubleIt = x -> x * 2;
Function<String, Integer> lenThenDouble = length.andThen(doubleIt);

// Consumer<T>     — T → void
Consumer<String> printer = System.out::println;
Consumer<String> upper = s -> System.out.println(s.toUpperCase());
Consumer<String> both = printer.andThen(upper);

// Supplier<T>     — () → T
Supplier<String> greeting = () -> "Hello World!";
System.out.println(greeting.get());

// BiFunction<T, U, R>  — (T, U) → R
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

// UnaryOperator<T> — T → T
UnaryOperator<String> upper2 = String::toUpperCase;

// BinaryOperator<T> — (T, T) → T
BinaryOperator<Integer> sum = Integer::sum;

Method References

// Method reference: shorthand for lambdas that call one method
// Type                    Lambda                  Method Ref

// Static method
// n -> Integer.parseInt(n)    →    Integer::parseInt

// Instance method (unbound)
// s -> s.length()              →    String::length

// Instance method (bound)
// () -> myObj.toString()       →    myObj::toString

// Constructor
// () -> new ArrayList<>()      →    ArrayList::new

// Examples:
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
names.forEach(System.out::println);       // instance (unbound)
names.sort(String::compareToIgnoreCase);  // instance (unbound)

List<Integer> nums = Arrays.asList(3, 1, 2);
nums.sort(Integer::compareTo);            // instance (unbound)
nums.stream().map(String::valueOf);       // static

Supplier<List<String>> listFactory = ArrayList::new;  // constructor
List<String> newList = listFactory.get();

Lambda Internals — invokedynamic

// Lambda is NOT an anonymous inner class at runtime!
// Java uses 'invokedynamic' bytecode instruction

// What happens:
// 1. Compiler generates invokedynamic call site
// 2. First call: LambdaMetafactory creates implementation class
// 3. Subsequent calls: use cached implementation (fast!)

// Difference from anonymous inner class:
// Anon class: new class file generated (.class)
// Lambda: no new class at compile time; generated at runtime
//         = smaller bytecode, faster loading

// Variable capture: lambdas can capture EFFECTIVELY FINAL vars
String prefix = "Hello";  // effectively final (never reassigned)
Consumer<String> greeter = name -> System.out.println(prefix + " " + name);

// prefix = "Hi";  // would break the lambda!

Interview Questions

What is a functional interface?

An interface with EXACTLY ONE abstract method (SAM — Single Abstract Method). Annotated with @FunctionalInterface (optional but recommended). Can have default/static methods. Examples: Runnable, Comparator, Callable, all java.util.function interfaces.

What variables can a lambda capture?

Lambdas can capture local variables that are effectively final (never reassigned after initialization). Instance variables and static variables are always accessible. This restriction prevents threading issues with mutable captured state.

Advertisement

java.util.function — Complete Reference

// ── BASIC INTERFACES ──
Predicate<T>          // T -> boolean
Function<T,R>         // T -> R
Consumer<T>           // T -> void
Supplier<T>           // () -> T
UnaryOperator<T>      // T -> T (extends Function<T,T>)
BinaryOperator<T>     // (T,T) -> T (extends BiFunction<T,T,T>)

// ── BI- VARIANTS ──
BiPredicate<T,U>      // (T,U) -> boolean
BiFunction<T,U,R>     // (T,U) -> R
BiConsumer<T,U>       // (T,U) -> void

// ── PRIMITIVE SPECIALIZATIONS (avoid boxing) ──
// INT
IntPredicate          // int -> boolean
IntFunction<R>        // int -> R
IntConsumer           // int -> void
IntSupplier           // () -> int
IntUnaryOperator      // int -> int
IntBinaryOperator     // (int,int) -> int
ToIntFunction<T>      // T -> int
ToIntBiFunction<T,U>  // (T,U) -> int
IntToLongFunction     // int -> long
IntToDoubleFunction   // int -> double
ObjIntConsumer<T>     // (T,int) -> void

// LONG
LongPredicate         // long -> boolean
LongFunction<R>       // long -> R
LongConsumer          // long -> void
LongSupplier          // () -> long
LongUnaryOperator     // long -> long
LongBinaryOperator    // (long,long) -> long
ToLongFunction<T>     // T -> long
ToLongBiFunction<T,U> // (T,U) -> long
LongToIntFunction     // long -> int
LongToDoubleFunction  // long -> double
ObjLongConsumer<T>    // (T,long) -> void

// DOUBLE
DoublePredicate       // double -> boolean
DoubleFunction<R>     // double -> R
DoubleConsumer        // double -> void
DoubleSupplier        // () -> double
DoubleUnaryOperator   // double -> double
DoubleBinaryOperator  // (double,double) -> double
ToDoubleFunction<T>   // T -> double
ToDoubleBiFunction<T,U> // (T,U) -> double
DoubleToIntFunction   // double -> int
DoubleToLongFunction  // double -> long
ObjDoubleConsumer<T>  // (T,double) -> void

// ── EXAMPLES ──
// IntPredicate: check if positive (no boxing!)
IntPredicate isPositive = n -> n > 0;
isPositive.test(5);  // true
isPositive.test(-1); // false

// IntBinaryOperator: sum without boxing
IntBinaryOperator sum = Integer::sum;
sum.applyAsInt(5, 3); // 8

// ToIntFunction: get length
ToIntFunction<String> length = String::length;
length.applyAsInt("Hello"); // 5

// IntStream uses these internally
IntStream.range(1,5).filter(isPositive).sum(); // avoids Integer objects

// ── COMPOSING PREDICATES ──
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive2 = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive2);
Predicate<Integer> isEvenOrPositive = isEven.or(isPositive2);
Predicate<Integer> isOdd = isEven.negate();
Predicate<Object> isNull = Predicate.not(Objects::nonNull); // Java 11

// ── COMPOSING FUNCTIONS ──
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, Integer> length2 = String::length;

Function<String, String> trimThenUpper = trim.andThen(upper); // trim → upper
Function<String, String> upperAfterTrim = upper.compose(trim); // trim first, then upper
Function<String, Integer> getLength = trim.andThen(length2);

// ── COMPOSING CONSUMERS ──
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info(s);
Consumer<String> printAndLog = print.andThen(log); // both run in order

// ── CUSTOM FUNCTIONAL INTERFACE ──
@FunctionalInterface
interface TriFunction<A,B,C,R> {
    R apply(A a, B b, C c);
}
TriFunction<Integer,Integer,Integer,Integer> triSum = (a,b,c) -> a+b+c;
triSum.apply(1,2,3); // 6

Optional Class

Optional to avoid NullPointerException

import java.util.Optional;

// Creating Optional
Optional<String> empty = Optional.empty();
Optional<String> present = Optional.of("Hello");
Optional<String> nullable = Optional.ofNullable(null);  // empty
Optional<String> nullable2 = Optional.ofNullable("Hi"); // present

// Checking and getting
present.isPresent();          // true
present.isEmpty();            // false (Java 11+)
present.get();                // "Hello" (throws if empty!)
empty.orElse("Default");      // "Default"
empty.orElseGet(() -> "Lazy");// computed only when empty
empty.orElseThrow(() -> new RuntimeException("Not found!"));

// Transforming
Optional<Integer> length = present.map(String::length);  // Optional[5]
Optional<String> upper = present.map(String::toUpperCase);

// FlatMap (when function returns Optional)
Optional<String> result = present.flatMap(
    s -> s.isEmpty() ? Optional.empty() : Optional.of(s));

// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);

// ifPresent
present.ifPresent(System.out::println);          // prints if present
present.ifPresentOrElse(                          // Java 9+
    System.out::println,
    () -> System.out.println("was empty")
);

// Stream (Java 9+)
present.stream().forEach(System.out::println);  // 0 or 1 element

// BAD usage: Optional in fields, parameters, collections
// GOOD usage: return type of methods that may not find a value
public Optional<User> findUserById(int id) {
    return users.stream().filter(u -> u.id == id).findFirst();
}

Interview Questions

What is Optional and when should you use it?

Optional is a container for a value that may or may not be present. Use it as a METHOD RETURN TYPE when the method may not find/produce a value (replaces returning null). Avoid: Optional fields, Optional parameters, Optional<Collection> (just return empty collection).