Skip to content
Java advanced 3 min read

Lambda Expressions

Lambda expressions (Java 8) give Java a concise way to represent behavior as data. A lambda is an anonymous function: a parameter list, an arrow, and a body. Combined with functional interfaces, lambdas let you pass logic to methods — the foundation of the Streams API and modern, declarative Java.

Syntax

A lambda has three parts: parameters, the -> arrow, and a body.

// Full form
(int a, int b) -> { return a + b; }

// Inferred types, single expression (implicit return)
(a, b) -> a + b

// Single parameter — parentheses optional
x -> x * 2

// No parameters
() -> System.currentTimeMillis()

The compiler infers parameter types from the target type — the functional interface the lambda is assigned to.

Functional Interfaces

A functional interface has exactly one abstract method (the SAM — Single Abstract Method). A lambda is an implementation of that method. java.util.function ships the core interfaces.

InterfaceAbstract methodMeaning
Function<T,R>R apply(T t)Transform a value
Predicate<T>boolean test(T t)Test a condition
Consumer<T>void accept(T t)Consume, no return
Supplier<T>T get()Produce a value
BiFunction<T,U,R>R apply(T t, U u)Two-arg transform
UnaryOperator<T>T apply(T t)T → T
import java.util.function.*;

Function<String, Integer> length = s -> s.length();
Predicate<Integer> isEven = n -> n % 2 == 0;
Consumer<String> printer = System.out::println;
Supplier<Double> random = Math::random;

System.out.println(length.apply("hello"));
System.out.println(isEven.test(4));
printer.accept("done");

Output:

5
true
done

Note: Annotate your own functional interfaces with @FunctionalInterface. The compiler then enforces the single-abstract-method rule and rejects accidental additions.

Method References

When a lambda merely calls an existing method, a method reference (::) is clearer. There are four kinds.

KindSyntaxExample
Static methodClass::staticMethodInteger::parseInt
Instance of a particular objectinstance::methodSystem.out::println
Instance of an arbitrary objectClass::instanceMethodString::toUpperCase
ConstructorClass::newArrayList::new
import java.util.*;
import java.util.stream.Collectors;

List<String> names = List.of("ada", "grace", "linus");

List<String> upper = names.stream()
    .map(String::toUpperCase)   // arbitrary-object instance ref
    .collect(Collectors.toList());

Supplier<List<String>> factory = ArrayList::new; // constructor ref

System.out.println(upper);

Output:

[ADA, GRACE, LINUS]

Effectively-Final Capture

A lambda can read local variables from its enclosing scope, but only if they are effectively final — assigned once and never reassigned.

int factor = 3;              // effectively final
Function<Integer, Integer> scale = n -> n * factor;
System.out.println(scale.apply(10)); // 30
// factor = 4; // would break the lambda: compile error

Output:

30

Warning: Reassigning a captured variable causes a compile error. This restriction prevents subtle bugs where a lambda would observe a value that changed after capture. Instance and static fields are not restricted because they live on the heap.

Lambdas vs Anonymous Classes

Lambdas often replace anonymous classes, but they differ in important ways.

// Anonymous class
Runnable a = new Runnable() {
    @Override public void run() {
        System.out.println(this.getClass()); // refers to the anon class
    }
};

// Lambda
Runnable b = () -> System.out.println("lambda");
AspectAnonymous classLambda
thisThe anonymous instanceThe enclosing instance
New .class fileYesNo (invokedynamic)
Can have state/fieldsYesNo
ImplementsAny interface/classFunctional interface only
Shadowing variablesAllowedNot allowed

Lambdas are lighter at runtime (no extra class loaded) and clearer for single-method behavior. Use an anonymous class when you need fields, multiple methods, or to extend a class.

Best Practices

  • Keep lambda bodies short; extract complex logic into named methods and reference them.
  • Prefer method references over lambdas when they read more clearly.
  • Name your custom functional interfaces by intent and annotate with @FunctionalInterface.
  • Keep lambdas pure — avoid side effects, especially inside stream pipelines.
  • Do not capture mutable state; rely on effectively-final locals or pass values explicitly.
  • Favor standard java.util.function interfaces over reinventing your own.
Last updated June 1, 2026
Was this helpful?