Skip to content

Lambda Expressions — Syntax, Target Typing, Variable Capture, and Method References

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Lambda Expressions. We cover key concepts, practical examples, and best practices to help you master this topic.

Lambda expressions in Java enable Functional Programming by providing concise syntax for implementing functional interfaces. Introduced in Java 8, lambdas transformed Java programming by allowing behavior to be passed as data — making code more expressive and reducing boilerplate compared to anonymous classes.

What You'll Learn

  • Lambda expression syntax and variations
  • Target typing and functional interfaces
  • Variable capture (effectively final)
  • Method references (:: operator)

Why It Matters

Lambdas are the foundation of the Stream API, Optional, CompletableFuture, and virtually every modern Java library. Without understanding lambdas, you cannot effectively use Java 8+ APIs.

Real-World Use

Sorting with Comparator.comparing(), event handlers (JavaFX), thread creation (new Thread(() -> { ... })), and every Stream pipeline relies on lambdas.


Lambda Syntax

// Full syntax: (parameters) -> { body }
(int a, int b) -> { return a + b; }

// Single parameter, no type
a -> a * 2

// No parameters
() -> System.out.println("Hello")

// Multiple statements
(String s) -> {
    String upper = s.toUpperCase();
    System.out.println(upper);
    return upper;
}

Concise Forms

// Type inference — compiler knows types from context
Comparator<String> byLength = (s1, s2) -> Integer.compare(s1.length(), s2.length());

// Single expression — no braces or return needed
Function<String, Integer> lengthFunc = s -> s.length();

// Multiple parameters — parentheses required
BinaryOperator<Integer> sum = (a, b) -> a + b;

Target Typing

The compiler infers the lambda's type from the context:

// Assigning to a functional interface variable
Predicate<String> isEmpty = s -> s.isEmpty();

// Passing as an argument
list.sort((a, b) -> a.compareTo(b));

// Returning from a method
public Predicate<Integer> isGreaterThan(int threshold) {
    return n -> n > threshold;
}

The @FunctionalInterface Annotation

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

Variable Capture

Lambdas can capture variables from the enclosing scope, but they must be effectively final:

String prefix = "Hello: ";
Function<String, String> greet = name -> prefix + name; // OK (prefix is effectively final)

int multiplier = 2;
Function<Integer, Integer> doubler = x -> x * multiplier; // OK

// ERROR: variable must be effectively final
int counter = 0;
Runnable increment = () -> counter++; // COMPILE ERROR

Why Effectively Final?

Lambdas capture the variable's value at the point of creation. If the variable could change, the lambda's behavior would be ambiguous. This also enables the JVM to avoid heap-allocating captured variables.

Mutable Capture Workaround

Use an array or AtomicInteger to work around the restriction:

int[] counter = {0};
Runnable increment = () -> counter[0]++; // OK (the reference is final, the content is mutable)

// Or use AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
Runnable increment = counter::incrementAndGet;

this in Lambdas vs Anonymous Classes

In an anonymous class, this refers to the anonymous instance. In a lambda, this refers to the enclosing instance:

public class ThisDemo {
    private String value = "Outer";

    public void test() {
        Runnable anonymous = new Runnable() {
            private String value = "Anonymous";
            @Override
            public void run() {
                System.out.println(this.value); // "Anonymous"
            }
        };

        Runnable lambda = () -> {
            System.out.println(this.value); // "Outer"
        };
    }
}

Method References

Method references are shorthand for lambdas that call a single method:

// Lambda
list.forEach(s -> System.out.println(s));

// Method reference
list.forEach(System.out::println);

Four Types

Type Syntax Lambda Equivalent
Static method ClassName::staticMethod (args) -> ClassName.staticMethod(args)
Instance method on a specific object instance::method (args) -> instance.method(args)
Instance method on an arbitrary object ClassName::instanceMethod (obj, args) -> obj.instanceMethod(args)
Constructor ClassName::new (args) -> new ClassName(args)
// Static method
Function<String, Integer> parseInt = Integer::parseInt;

// Instance method on specific object
Consumer<String> print = System.out::println;

// Instance method on arbitrary object
Function<String, Integer> getLength = String::length;
Comparator<String> byCase = String::compareToIgnoreCase;

// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;
Function<String, Person> personCreator = Person::new;

Common Mistakes

  1. Using lambdas for multi-line logic. If a lambda has more than 3-4 lines, extract it into a named method and use a method reference.
  2. Modifying captured variables. The effectively final restriction catches this at compile time. Use AtomicInteger or an array instead.
  3. Confusing lambda with anonymous class this. this in a lambda refers to the enclosing instance, not the lambda itself.
  4. Omitting parentheses for zero-parameter lambdas. () -> 42 is correct; -> 42 is not.
  5. Using lambdas when method references are clearer. Person::getName is better than p -> p.getName().

Practice Questions

1. What is a functional interface?
An interface with exactly one abstract method. Lambdas can be used to instantiate functional interfaces.

2. What does "effectively final" mean for lambda captures?
A variable is effectively final if it is not reassigned after initialization. Lambdas can capture such variables.

3. How do method references differ from lambdas?
Method references are a shorthand for lambdas that call exactly one existing method. They are more concise but less flexible than lambdas.

4. Why does this behave differently in lambdas vs anonymous classes?
In anonymous classes, this refers to the anonymous class instance. In lambdas, this refers to the enclosing class — lambdas do not introduce a new scope.

5. Can a lambda have multiple abstract methods?
No. Lambda expressions are designed for functional interfaces (single abstract method). For multiple methods, use an anonymous class.

Challenge Question:
Create a fluent logger API using functional interfaces:

Logger logger = Logger.withPrefix("[APP] ");
logger.info("Starting application");
logger.info("User logged in: {}", "Alice");

Define Logger with methods that accept lambdas and Supplier for lazy evaluation. The info method should only evaluate the message supplier if the log level is enabled.

FAQ

What is the compilation target for lambdas?

Lambdas compile to invokedynamic instructions (Java 7+). The JVM links the lambda to the functional interface at runtime. This is more efficient than anonymous classes, which generate a separate .class file.

Can I serialize a lambda?

Not generally. The lambda's synthetic type is not guaranteed to be serializable. If you need serialization, use an anonymous class or a named inner class.

What happens if a lambda throws a checked exception?

The exception must be compatible with the functional interface's method signature. If the interface method does not declare the exception, you cannot throw it from the lambda (without wrapping in a RuntimeException).

Can I use `var` in lambda parameters?

Yes, Java 11+ allows var in lambda parameters: (@Nullable var x, var y) -> x + y. This is useful when you want to add annotations to lambda parameters.

What is the difference between `list.forEach(System.out::println)` and `list.forEach(s -> System.out.println(s))`?

Both produce the same output. The method reference System.out::println is more concise and idiomatic for Java 8+ code.

Mini Project

Write a program LambdaDemo.java that:

  1. Creates a list of words and sorts them by length using a lambda
  2. Filters words starting with "A" using Predicate lambda and Stream.filter()
  3. Transforms each word to uppercase using Function lambda
  4. Creates a custom @FunctionalInterface StringProcessor with method String Process(String input), and implements it with three different lambdas: trim, reverse, and uppercase
  5. Uses constructor reference ArrayList::new to create a new list
  6. Demonstrates effectively final capture with a counter
  7. Shows the this difference between lambda and anonymous class

What's Next

Lambdas enable functional programming, and the Stream API is their primary use case. Lesson 34 covers Stream API — creating streams, intermediate operations (map, filter, flatMap), terminal operations (collect, reduce, forEach), and parallel streams.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro