Java Lambda Expressions

ADVANCED ~8 min read Tutorial

A lambda expression is a concise way to pass behaviour as data. Lambdas implement functional interfaces (interfaces with a single abstract method) in a single line. They are the foundation of the modern Java functional style, used heavily with the Streams API.

This tutorial covers lambda syntax, functional interfaces, method references, capturing variables, and the standard functional interfaces in java.util.function.

1. Lambda Syntax

A lambda has three parts: a parameter list, an arrow ->, and a body. The body can be a single expression or a block:

java
class=class="tok-str">"tok-cmt">// single expression body
Runnable r = () -> System.out.println("hi");
Comparator<Integer> cmp = (a, b) -> a - b;

class=class="tok-str">"tok-cmt">// block body with return
Function<Integer, Integer> square = (Integer n) -> {
    int result = n * n;
    return result;
};

class=class="tok-str">"tok-cmt">// no parameters
Runnable noop = () -> {};

class=class="tok-str">"tok-cmt">// one parameter without parentheses
Function<String, Integer> length = s -> s.length();

class=class="tok-str">"tok-cmt">// multiple parameters
BinaryOperator<Integer> sum = (a, b) -> a + b;

class=class="tok-str">"tok-cmt">// with type inference explicit
BinaryOperator<Integer> sum2 = (Integer a, Integer b) -> a + b;

The compiler infers the parameter types from the target functional interface. You can omit them entirely. For a single-parameter lambda, the parentheses around the parameter are also optional.

2. Functional Interfaces

A functional interface has exactly one abstract method. The @FunctionalInterface annotation makes the compiler check this rule:

java
@FunctionalInterface
public interface StringFunction {
    String apply(String input);
}

class=class="tok-str">"tok-cmt">// usage
StringFunction upper = s -> s.toUpperCase();
StringFunction exclaim = s -> s + "!";

String result = upper.apply("hello");   class=class="tok-str">"tok-cmt">// "HELLO"

class=class="tok-str">"tok-cmt">// built-in functional interface
java.util.function.Function<String, Integer> len = s -> s.length();
java.util.function.Predicate<String> isEmpty = s -> s.isEmpty();
java.util.function.Consumer<String> print = s -> System.out.println(s);
java.util.function.Supplier<List<String>> empty = () -> new ArrayList<>();

Lambdas are not objects in the JVM sense — the compiler generates an implementation of the target interface at compile time, and the JVM may reuse a single instance if the lambda does not capture state.

3. Method References

When a lambda just calls a single method, you can replace it with a method reference — ClassName::methodName. Four kinds:

java
class=class="tok-str">"tok-cmt">// class="tok-num">1. static method reference
Function<String, Integer> parser = Integer::parseInt;
class=class="tok-str">"tok-cmt">// equivalent to: s -> Integer.parseInt(s)

class=class="tok-str">"tok-cmt">// class="tok-num">2. instance method reference (on a specific object)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
class=class="tok-str">"tok-cmt">// equivalent to: s -> prefix.concat(s)

class=class="tok-str">"tok-cmt">// class="tok-num">3. instance method reference (unbound, first param becomes the receiver)
Function<String, Integer> length = String::length;
class=class="tok-str">"tok-cmt">// equivalent to: s -> s.length()

class=class="tok-str">"tok-cmt">// class="tok-num">4. constructor reference
Supplier<ArrayList<String>> factory = ArrayList::new;
class=class="tok-str">"tok-cmt">// equivalent to: () -> new ArrayList<>()
Function<Integer, String[]> newArray = String[]::new;
class=class="tok-str">"tok-cmt">// equivalent to: n -> new String[n]

Method references are nearly always more readable than the equivalent lambda. Reach for them when the lambda body is a single method call.

4. Capturing Variables

A lambda can use variables from its enclosing scope. The captured variables must be effectively final — assigned once and never changed:

java
class=class="tok-str">"tok-cmt">// captured local - must be effectively final
String greeting = "Hello, ";
Runnable r = () -> System.out.println(greeting + "world");
class=class="tok-str">"tok-cmt">// greeting = "Hi, ";  // would be a compile error - lambda captures greeting

class=class="tok-str">"tok-cmt">// captured effectively-final parameter
public void process(List<String> names, String suffix) {
    names.forEach(n -> System.out.println(n + suffix));
    class=class="tok-str">"tok-cmt">// suffix is effectively final - never reassigned in this method
}

class=class="tok-str">"tok-cmt">// mutable workaround - capture an array reference
int[] counter = {class="tok-num">0};
Runnable inc = () -> counter[class="tok-num">0]++;
inc.run();
inc.run();
System.out.println(counter[class="tok-num">0]);   class=class="tok-str">"tok-cmt">// class="tok-num">2

This restriction enables safe concurrent execution: the captured values are immutable snapshots, so a lambda running on another thread cannot see a torn read.

5. Standard Functional Interfaces

The java.util.function package has dozens of functional interfaces. The most-used:

InterfaceMethodUse
Predicate<T>boolean test(T)Filter / boolean test
Consumer<T>void accept(T)Side effect on input
Supplier<T>T get()Produce a value
Function<T,R>R apply(T)Transform input to output
BiFunction<T,U,R>R apply(T,U)Two inputs, one output
UnaryOperator<T>T apply(T)Input and output same type
BinaryOperator<T>T apply(T,T)Combine two values
java
import java.util.function.*;

class=class="tok-str">"tok-cmt">// Predicate - boolean test
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> notEmpty = isEmpty.negate();
boolean empty = isEmpty.test("");

class=class="tok-str">"tok-cmt">// Consumer - side effect
Consumer<String> printer = System.out::println;
Consumer<String> debug = printer.andThen(s -> System.err.println("DEBUG: " + s));

class=class="tok-str">"tok-cmt">// Supplier - produce a value
Supplier<Double> random = Math::random;
double v = random.get();

class=class="tok-str">"tok-cmt">// Function - transform
Function<String, Integer> length = String::length;
Function<Integer, String> toStr = Object::toString;

class=class="tok-str">"tok-cmt">// BiFunction - two args
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

class=class="tok-str">"tok-cmt">// UnaryOperator - same input/output type
UnaryOperator<String> trim = String::trim;

class=class="tok-str">"tok-cmt">// BinaryOperator - combine two of same type
BinaryOperator<Integer> max = Integer::max;

6. Composing Functions

Function and Predicate compose with default methods like andThen and compose:

java
import java.util.function.Function;

Function<Integer, Integer> plusOne = x -> x + class="tok-num">1;
Function<Integer, Integer> timesTwo = x -> x * class="tok-num">2;

class=class="tok-str">"tok-cmt">// andThen - apply this, then the other
Function<Integer, Integer> combined = plusOne.andThen(timesTwo);
class=class="tok-str">"tok-cmt">// combined.apply(class="tok-num">3) = (class="tok-num">3 + class="tok-num">1) * class="tok-num">2 = class="tok-num">8

class=class="tok-str">"tok-cmt">// compose - apply the other first, then this
Function<Integer, Integer> composed = plusOne.compose(timesTwo);
class=class="tok-str">"tok-cmt">// composed.apply(class="tok-num">3) = (class="tok-num">3 * class="tok-num">2) + class="tok-num">1 = class="tok-num">7

class=class="tok-str">"tok-cmt">// predicate composition
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> longerThan3 = s -> s.length() > class="tok-num">3;
Predicate<String> valid = notEmpty.and(longerThan3);
boolean ok = valid.test("hello");   class=class="tok-str">"tok-cmt">// true

7. Pitfalls

Lambdas are not closures over mutable state

int[] counter = {0}; nums.forEach(n -> counter[0]++); compiles — the array reference is effectively final, but its contents are mutated. This works but is fragile; use a stream reduction (count()) or an AtomicInteger instead.

Lambdas and exceptions

A lambda cannot throw a checked exception unless the functional interface's method declares it. If you need to call a checked-exception-throwing method from a lambda, either catch inside the lambda or wrap the exception in a runtime exception.

Exercises

  1. Rewrite an anonymous Comparator<String> as a lambda, then as a method reference.
  2. Use Predicate<String> to filter a list of strings to those longer than 4 characters.
  3. Use Function<String, Integer> to map strings to their lengths, then compose with another function that doubles the length.
  4. Use Supplier<List> with Stream.generate to create an infinite stream, then limit it.