Anonymous callbacks and capture: what runs later?

Trace one-off Java behavior, functional-interface lambdas, and Python closures without confusing creation, capture, and invocation. Methods and type contracts · Object-oriented programming.

You hand a task to another part of a program and expect it to run later. The task might be a Java lambda, an inline Java object, or a Python function. The useful question is not “is this anonymous?” It is “what was created, what did it capture, and when does its body run?”

Create now, run later

A lambda is a short way to write the one method an interface requires: () -> ... means “a method taking nothing that does ...”. Java calls an interface with exactly one abstract method a functional interface, and Job below is one, so a lambda can supply its run method. The scenes show printed output on a Console card. Follow the three moments in order: the callback is created, it captures what it mentions, and a later run() call executes the body.

A callback that waits for its call

Read the steps as text

  1. The local prefix refers to StringBuilder B0, which holds A. Nothing has been printed, and task refers to no callback yet.
  2. The program builds a new Job T0. Variable task now refers to Job T0. Job T0's captured link now points at StringBuilder B0. Creating the lambda runs no body, so nothing is printed and the callback only holds the StringBuilder.
  3. The program changes the text of StringBuilder B0 from "A" to "A+". The callback holds the StringBuilder itself, not a snapshot, so it already refers to the changed object.
  4. The program changes the output of Console C0 from "nothing yet" to "A+-sent". Only this call runs the body, and it reads the StringBuilder as it is at this moment.
  5. Creating the callback printed nothing. Calling run() printed A+-sent.
interface Job {
    void run();
}

StringBuilder prefix = new StringBuilder("A");
Job task = () -> System.out.println(prefix + "-sent"); // created, nothing printed
prefix.append("+");                                      // the same builder changes
task.run();                                              // prints A+-sent

Nothing prints when the lambda is evaluated. The callback holds a link to the StringBuilder that prefix names, so append is visible when the body finally reads it. Java did not freeze the object, because it captured the reference.

Java does insist that the local variable stays put. A local used inside a lambda or an anonymous class must be final or effectively final, meaning it is never reassigned. Writing prefix = new StringBuilder("B"); after the lambda would be a compile error, since the callback would silently keep the old object.

Anonymous class or lambda: whose this?

An anonymous class, written new Job() { ... }, creates one object with no reusable class name. It still has an identity, it can declare its own members, and when it is built inside an instance method it keeps a link to the enclosing object. A lambda is shorter and adds no object-like receiver of its own, so this inside it means the surrounding object.

Whose this is it?

Read the steps as text

  1. Dispatcher N0 has who set to "dispatcher". Its demo method is about to build two callbacks, anon and lam, and nothing has been printed.
  2. The program builds a new Job A0. Variable anon now refers to Job A0. Job A0's enclosing link now points at Dispatcher N0. The anonymous object has its own who and an enclosing link to the Dispatcher that built it.
  3. The program builds a new Job L0. Variable lam now refers to Job L0. Job L0's this link now points at Dispatcher N0. The lambda adds no receiver of its own, so its this is the Dispatcher.
  4. The program changes the output of Console C0 from "nothing yet" to "callback". Inside the anonymous body, this is the callback object, so who is its own.
  5. The program changes the output of Console C0 from "callback" to "callback / dispatcher". Inside the lambda body, this is still the Dispatcher, so who is the Dispatcher's.
  6. Both callbacks reach the Dispatcher, but only the anonymous object also owns a who.
class Dispatcher {
    String who = "dispatcher";

    void demo() {
        Job anon = new Job() {
            String who = "callback";
            public void run() { System.out.println(this.who); }
        };
        Job lam = () -> System.out.println(this.who);
        anon.run();   // callback
        lam.run();    // dispatcher
    }
}

The anonymous body sees its own who through this. The lambda body has no who of its own, so this.who reads the Dispatcher's. That matters whenever a callback touches instance state: choose a lambda to stay in the surrounding object's context, and an anonymous class when the callback needs state or members of its own.

Python closures look names up when called

A Python lambda is an anonymous function object, and a nested def is a named one; both can travel as callbacks. When the body mentions a name from an enclosing scope, the function keeps a link to the variable's cell, and Python reads the cell when the function is called. Rebinding the variable later therefore changes what the function sees.

late = [lambda: i for i in range(3)]
snap = [lambda i=i: i for i in range(3)]

[f() for f in late]   # [2, 2, 2]
[f() for f in snap]   # [0, 1, 2]

The scenes unroll each comprehension into three assignments and one print.

All three late lambdas link to the one cell for i, and the loop leaves it at 2. Each call reads the cell only now, so every lambda reports 2.

Three lambdas, one cell

Read the steps as text

  1. A loop is about to build three lambdas that each read i. The cell for i holds 0, nothing has been printed, and f0, f1 and f2 refer to nothing yet.
  2. The program builds a new Function F0. Variable f0 now refers to Function F0. Function F0's cell link now points at Cell I0. The lambda stores a link to the cell for i, not the number it holds right now.
  3. The program changes the value of Cell I0 from 0 to 1. The loop changes the cell in place, and the first lambda still links to it.
  4. The program builds a new Function F1. Variable f1 now refers to Function F1. Function F1's cell link now points at Cell I0. The second lambda links to that same cell.
  5. The program changes the value of Cell I0 from 1 to 2. The cell moves again, and both lambdas link to a cell that now holds 2.
  6. The program builds a new Function F2. Variable f2 now refers to Function F2. Function F2's cell link now points at Cell I0. All three lambdas link to one cell, and none holds a number of its own.
  7. The program changes the output of Console C0 from "nothing yet" to "2 2 2". Each call looks the cell up only now, so even the first lambda reports what the cell holds at call time.
  8. One cell with three lambdas linked to it explains the repeated 2.

A default argument is evaluated when the lambda is created, so i=i stores the number from that pass inside the function itself. It is a deliberate snapshot, not a rule for every closure. A default that holds a mutable object, such as a list, stores the reference, so later changes to that list still show.

A default argument keeps its own value

Read the steps as text

  1. The same loop now builds each lambda with i=i. The cell for i still holds 0, nothing has been printed, and no lambda exists yet.
  2. The program builds a new Function S0. Variable f0 now refers to Function S0. The default i=i is evaluated now, so this lambda stores the number 0 itself.
  3. The program changes the value of Cell I1 from 0 to 1. The program builds a new Function S1. Variable f1 now refers to Function S1. The cell moves to 1, and the new lambda stores 1 while the first one keeps 0.
  4. The program changes the value of Cell I1 from 1 to 2. The program builds a new Function S2. Variable f2 now refers to Function S2. The loop ends with the cell at 2, and the third lambda stores 2.
  5. The program changes the output of Console C0 from "nothing yet" to "0 1 2". Each call reads the number its own lambda stored, so the final value of the cell no longer matters.
  6. Only the name i links to the cell, because the lambdas keep numbers rather than links.

Choose what the callback should see

Decide first whether the callback should observe later changes or keep today's value. To observe changes, capture the object, as in the StringBuilder above. To keep a value as of now, copy it into a new local before creating the callback in Java, such as String now = prefix.toString();, or use a default argument in Python. A button handler registered in a loop, or a timer that reports a setting, is where this timing shows up. Java's single-method interface and effectively-final rule have no Python counterpart, so predict each language by its own rule.

For any delayed call, name three moments: creation, capture, invocation. Then ask which object the captured link reaches and whether anything changed it before the call.

Recap

  • A callback is created, captures, and runs later; its body executes only at the call.
  • Java captures the value of an effectively-final local, which for an object is the reference, so a mutation shows and a reassignment is rejected. A lambda's this is the surrounding object, while an anonymous object has its own.
  • Python closures read the variable's cell when called; a default argument stores a value at creation.
Go deeper

Java anonymous classes can declare extra members and use their own this; lambdas cannot declare a new class body and use lexical this. Java method references are another way to create a functional-interface value, but they do not change the creation-versus-invocation timing. In Python, nonlocal can rebind a closure cell, which makes later calls observe the new value; a default argument remains the value evaluated for that function object. None of these rules requires an event loop or a thread: “later” can mean the next ordinary method call.

Revisit method receivers and class methods when the callback's receiver matters, and revisit nested types and enclosing scope when you need to name the enclosing object or closure that a callable carries. Continue to exception contracts when a callback can fail and its caller must choose an explicit failure contract. Practise object updates in Linked List Fundamentals, where the operation runs against a concrete node state.