CodeGym /Courses /JAVA 25 SELF /Lambda expressions: syntax, scopes

Lambda expressions: syntax, scopes

JAVA 25 SELF
Level 48 , Lesson 0
Available

1. Introduction

Let’s be honest: writing anonymous classes for one or two lines of code is like hiring a huge truck to deliver a single bun from the bakery to the shop.

For example, if you want to sort a list of strings by length, before Java 8 you had to write this:

List<String> list = Arrays.asList("apple", "banana", "kiwi");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

The task is simple, yet the code takes up half a screen. It’s especially annoying when there are many such operations: the code becomes “noisy,” and the essence of the task gets lost. Developers cried, suffered, and then invented lambda expressions. We’ve already studied them a bit; now we’ll review and deepen our knowledge.

What is a lambda expression

A lambda expression is a compact way to implement a functional interface, i.e., an interface with a single abstract method (for example, Comparator, Runnable, Consumer, and many others).

Simply put, a lambda expression lets you write a function “on the fly,” right where it’s needed, without declaring a separate class or method.

General syntax:

(parameters) -> { body }

Examples:

  • (a, b) -> a + b — a function that adds two numbers
  • x -> x * x — a function that squares a number
  • () -> System.out.println("Hello!") — a function with no parameters

Relationship with functional interfaces:
A lambda expression can always be assigned to a variable of a functional interface type or passed as an argument to a method that expects such an interface.

2. Syntax of lambda expressions

No parameters

Runnable r = () -> System.out.println("Hello, world!");
r.run(); // Prints: Hello, world!

Single parameter
If there’s a single parameter, you can omit the parentheses:

Consumer<String> print = s -> System.out.println(s);
print.accept("Java is cool!");

Multiple parameters
Parentheses are required:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

Single-expression body
If the body consists of a single expression, braces and return are not needed:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

Block body
If you need several statements, use braces and return (if there’s a return value):

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

Parameter types
Most of the time you can omit parameter types—the compiler infers them from context. But if you want, you can specify them explicitly:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

Lambda with no return value
If the interface returns void, just write the statements:

list.forEach(s -> System.out.println("Element: " + s));

Table: Syntax variations for lambda expressions

What Example Comment
No parameters
() -> System.out.println("Hi")
For example, for Runnable
Single parameter
x -> x * x
Parentheses can be omitted
Multiple parameters
(a, b) -> a + b
Parentheses are required
Single expression
x -> x + 1
No return or braces
Block of code
x -> { int y = x + 1; return y * 2; }
Use return if there is a result

3. Usage: where and how to use lambda expressions

Lambda expressions are most often used where you need to pass “behavior” — a function — as an argument. This was a real revolution for collections, streams (Stream API), events, and much more.

Sorting a list

Before Java 8:

list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

With a lambda expression:

list.sort((a, b) -> a.length() - b.length());

Creating a thread (Thread)

Thread t = new Thread(() -> System.out.println("Thread started!"));
t.start();

Processing collections

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

Filtering a list

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

Example: Building out our learning app

Suppose we have a list of users:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

Print all users whose names are longer than 4 characters:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("User: " + name));

4. Variable scope in lambda expressions

Lambda expressions can use variables from the enclosing method, but there are gotchas!

Variables and lambdas

A lambda in Java can capture only those variables that do not change after initialization. If a variable is declared final — that’s obvious. But even if the word final isn’t written, the compiler checks whether the value changes. If it doesn’t, it treats it as “effectively final” and happily allows it in the lambda.

Example:

int minLength = 4; // value never changes
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

This works because minLength stays the same number.

But if after using it in the lambda you try to reassign minLength, you’ll get a compilation error:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // Error! The lambda has already "captured" the value

In essence, the rule is very simple: a variable used inside a lambda must be immutable.

Difference from anonymous classes

In anonymous classes and lambda expressions, variables from the outer method behave the same: only final/effectively final.

BUT!
For a lambda expression, this refers to the outer object (the current instance of the class), whereas for an anonymous class it refers to the instance of the anonymous class. This matters if you access fields or methods of the current class inside the lambda.

Example:

public class Example {
    String name = "Outer class";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "Anonymous class";
            @Override
            public void run() {
                System.out.println(this.name); // "Anonymous class"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "Outer class"

        r1.run();
        r2.run();
    }
}

5. Common mistakes when working with lambda expressions

Error #1: Using a non-final/not effectively final variable. If you decide to change a variable after using it in a lambda, the compiler will stop you. This is for safety: otherwise it would be unclear which value to use.

Error #2: Confusion with this. In a lambda expression, this is the outer class, while in an anonymous class it is the anonymous class itself. If you try to call a method of the outer class from a lambda, everything will work, but from an anonymous class it won’t (if you were relying on the outer class context).

Error #3: Lambda without a context. A lambda expression cannot be used by itself — you must either assign it to a functional interface variable or pass it where that interface is expected. Trying to just write x -> x + 1 outside of a context will cause an error.

Error #4: An overly complex lambda. If a lambda grows beyond 3–5 lines, it’s hard to read. In such cases, move the logic to a separate method.

1
Task
JAVA 25 SELF, level 48, lesson 0
Locked
Name Sorting Workshop 📜
Name Sorting Workshop 📜
1
Task
JAVA 25 SELF, level 48, lesson 0
Locked
Mystic Number Classifier 🔮
Mystic Number Classifier 🔮
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION