Skip to content
CodeKruPro

Predict the output · Java

Scope, closures and lambdas

Which variable a name refers to, and what a closure captures.

Learn: Scope, lambdas and captured variables in Java7 minute read

Scope is the part of a program where a name can be used. Scope questions ask which variable a name refers to and how long it lives. Lambda questions add one more rule: what a lambda (a short anonymous function such as x -> x * 2) is allowed to capture from the code around it. Testers meet these rules in data-driven tests, stream filters (code that picks items from a list with a stream) and listener code (code that reacts to an event), so interviewers check them. Java keeps them strict, which makes the output easy to predict.

Rule 1: a local variable lives inside its block

A block is the code between a pair of braces. A variable declared inside a block exists only until the closing brace. A variable declared in a loop body is created fresh on every pass, and the loop variable of a for loop is gone after the loop, so a new variable with the same name can be declared later. Java does not allow a local variable to shadow (hide) another local in an enclosing block (a block around it) of the same method, so that kind of confusion does not even compile: Java refuses to build the program.

Rule 2: locals and parameters shadow fields

A local variable or parameter with the same name as a field hides the field. This is called shadowing: plain x then means the local. Use this.x for an instance field or ClassName.x for a static field. The classic bug is size = size; in a constructor: it assigns the parameter to itself, and the field keeps its default value.

Rule 3: lifetime depends on where a variable is declared

A local variable is created again on each method call, so it never remembers anything between calls. An instance field lives as long as its object, and a static field lives as long as the class, shared by every object.

Rule 4: lambdas capture effectively final values

A lambda may use a local variable from the surrounding method only if that variable is effectively final, meaning it is never assigned again after it gets its value. The lambda captures that value. Inside a loop, declare a new variable on each pass (int j = i;) to give each lambda its own value. Fields are different: a lambda reaches them through this (or the class), so it can read and change them freely. A one-element array is a common way to let a lambda change a counter, because the array reference never changes. An AtomicInteger, a class that holds a number which can be changed safely, works too.

import java.util.*;
import java.util.function.*;

public class Main {
    static int total = 100;

    public static void main(String[] args) {
        int total = 1;
        List<IntSupplier> list = new ArrayList<>();
        for (int i = 1; i <= 3; i++) {
            int step = i * total;
            list.add(() -> step);
        }
        for (IntSupplier s : list) {
            System.out.print(s.getAsInt() + " ");
        }
        System.out.println();
        System.out.println(total + " " + Main.total);
    }
}
1 2 3
1 100

Here the local total shadows the static field, each lambda captures its own step, and Main.total still reaches the field.

Rule 5: this in a lambda is the enclosing object

An anonymous class is a class written without a name, right where it is used. A lambda does not start a new scope for this; inside it, this is the object whose method created the lambda. An anonymous class is a real class, so this inside it is the anonymous object, and Outer.this reaches the enclosing one.

Rule 6: method references fix their target early

A method reference is a short way to write a lambda that calls one method. In name::toUpperCase, the expression before :: is evaluated once, when the reference is created, and kept. Pointing name at another object later does not change what the reference calls.

Rule 7: streams are lazy

Intermediate operations such as map and filter only describe work. Nothing runs until a terminal operation (the final step that produces a result) such as collect or findFirst, and then elements flow through one at a time. Short-circuiting steps such as limit and findFirst stop early, so later elements are never processed.

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> s = Stream.of("a", "b", "c").map(x -> {
            System.out.println("map " + x);
            return x.toUpperCase();
        });
        System.out.println("built");
        System.out.println(s.limit(2).collect(Collectors.toList()));
    }
}
built
map a
map b
[A, B]

How to answer in an interview

For each name in the snippet, say which declaration it refers to before you trace values. If asked why a lambda cannot change a local counter, answer directly: "Java lambdas capture the value of effectively final locals, so assigning to the local is a compile error (Java refuses to build the program). I would use a field, an array element or an AtomicInteger." Mention that this rule is what protects Java from the late binding surprise that Python has.

Common mistakes

  • Writing size = size; and expecting the field to change.
  • Expecting a local variable to keep its value between calls.
  • Thinking a lambda's this is the lambda itself.
  • Expecting a stream's map to print before a terminal step runs.

Recap

Names resolve to the closest declaration, and this. or the class name reaches a hidden field. Locals restart on every call; static fields are shared. Lambdas capture effectively final values and keep the outer this, method references fix their target when created, and streams do nothing until a terminal step.

9 questions0/9 solved

Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan