Lambda Expressions in Java

A lambda is a short way to write an implementation of a single method interface, so behaviour can be passed around like a value.

Definition

A lambda expression is an anonymous function written inline. It supplies the implementation of the one abstract method of a functional interface, which is what lets behaviour be passed to a method as an argument.

Before and after

// Java 7 and earlier
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Working");
    }
};

// Java 8 onwards
Runnable task = () -> System.out.println("Working");

Syntax

(parameters) -> expression
(parameters) -> { statements; }
() -> 42                                  // no parameters
name -> name.length()                     // one parameter, brackets optional
(a, b) -> a + b                           // two parameters
(String a, String b) -> a.compareTo(b)    // explicit types, rarely needed
(var a, var b) -> a + b                   // var form, Java 11 onwards

value -> {                                // a block body needs return
    int doubled = value * 2;
    return doubled + 1;
}

The parameter types are inferred from the target type, which is why they are almost always omitted.

Where a lambda can be used

List<String> names = new ArrayList<>(List.of("Ravi", "Anita", "Meera"));

names.sort((a, b) -> a.compareTo(b));           // Comparator
names.forEach(name -> System.out.println(name)); // Consumer
names.removeIf(name -> name.startsWith("A"));   // Predicate

Supplier<LocalDate> today = () -> LocalDate.now();
Function<String, Integer> length = text -> text.length();

A lambda only works where the compiler knows the target type. That target must be a functional interface, an interface with exactly one abstract method.

// Object task = () -> System.out.println("x");   // compile error, no target type
Runnable task = () -> System.out.println("x");    // fine

A practical example

public class NoteFilter {

    public static List<Note> filter(List<Note> notes, Predicate<Note> condition) {
        List<Note> result = new ArrayList<>();
        for (Note note : notes) {
            if (condition.test(note)) {
                result.add(note);
            }
        }
        return result;
    }
}
List<Note> published = NoteFilter.filter(notes, note -> note.isPublished());
List<Note> recent = NoteFilter.filter(notes, note -> note.created().isAfter(cutoff));
List<Note> both = NoteFilter.filter(notes,
        note -> note.isPublished() && note.views() > 100);

One method, any number of filtering rules. Before lambdas this required either a class per rule or a flag parameter that grew with every new case.

Variable capture

int threshold = 100;                        // effectively final
Predicate<Note> popular = note -> note.views() > threshold;

// threshold = 200;                          // would break the lambda above

A lambda may use a local variable only if it is final or effectively final, meaning never reassigned. The value is captured, and allowing later reassignment would make the captured value ambiguous. Fields and static fields have no such restriction, because the lambda captures this rather than a copy.

// A common workaround that is usually a warning sign
int[] counter = {0};
names.forEach(name -> counter[0]++);      // legal, but mutable shared state

long count = names.stream().filter(name -> name.length() > 4).count();   // better

this inside a lambda

public class Service {

    private final String name = "search";

    public Runnable asLambda() {
        return () -> System.out.println(this.name);   // the Service instance
    }

    public Runnable asAnonymous() {
        return new Runnable() {
            @Override public void run() {
                System.out.println(Service.this.name);   // needs qualification
            }
        };
    }
}
A lambda does not create a new scope for this. It sees the enclosing instance directly, unlike an anonymous class. This is one of the practical reasons lambdas are easier to reason about.

Lambda compared with anonymous class

AspectLambdaAnonymous class
TargetFunctional interfaces onlyAny interface or class
Abstract methodsExactly oneAny number
Own fieldsNoYes
thisThe enclosing instanceThe anonymous instance
Compiled toAn invokedynamic call siteA separate class file
ReadabilityShortVerbose

Common mistakes

  • Assigning a lambda to a variable of a non functional type such as Object.
  • Reassigning a captured local variable.
  • Writing a lambda body of twenty lines. Extract a method and use a method reference.
  • Forgetting return in a block bodied lambda.
  • Using an array or a field to accumulate state inside a lambda, instead of the operation designed for it.
  • Catching checked exceptions awkwardly, because most built in functional interfaces do not declare any.

Checked exceptions in lambdas

// Does not compile: Function.apply declares no checked exception
// paths.stream().map(path -> Files.readString(path)).toList();

List<String> contents = paths.stream()
        .map(path -> {
            try {
                return Files.readString(path);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        })
        .toList();

When the body grows like this, move it into a private method and reference it instead.

Best practices

  • Keep lambdas to one expression where possible.
  • Extract anything longer than about three lines into a named method.
  • Prefer a method reference when the lambda only forwards its arguments.
  • Give parameters meaningful names; note reads better than n.
  • Keep lambdas free of side effects, especially inside streams.
  • Omit parameter types and let inference work.

Practice

  1. Convert an anonymous Comparator into a lambda and then into a method reference.
  2. Why does the compiler reject Object o = () -> {};?
  3. Write a method taking a Predicate<String> and call it with three different rules.
  4. Explain the effectively final restriction with a short example that fails to compile.
  5. Show what this refers to inside a lambda declared in an instance method.

Conclusion

A lambda is the implementation of a single method interface written where it is needed. It makes behaviour a value you can pass, and it is the foundation everything in the Stream API is built on.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.