Capturing of the variable has absolutely nothing to do with the concurrent execution or its safety, the reason is completely different.

Before I answer your questions, let me first explain what is a lambda expression.

What is lambda expression

When you use lambda expression, there are a few things happening, both during compilation and runtime, that are hidden from the developer. It's also worth nothing that lambda expression is part of the java language, it doesn't exist in the generated bytecode.

I'll use following code as an example

public class GreeterFactory {

    private String header = "Hello ";
    
    public Function<String, String> createGreeter(int greeterId){
        Function<String, String> greeter = username -> {
            return String.format("(%s) %s: %s", greeterId, header, username);
        };
        
        return greeter;
    }
}

lamba expression is compiled into anonymous method

When javac compiles java into bytecode, it'll convert your lambda's body into new method in the embedding class (that's why lambda expressions can be though of as anonymous methods).

Here's what will be in the bytecode (decompiled with javap tool):

Compiled from "GreeterFactory.java"
public class various.GreeterFactory {
  private java.lang.String header;
  public various.GreeterFactory();
  public java.util.function.Function<java.lang.String, java.lang.String> createGreeter(int);
  private java.lang.String lambda$createGreeter$0(int, java.lang.String);
}

As you can see the GreeterFactory class not only has the createGreeter method that I've written. It will also now have lambda$createGreeter$0 method that was generated by the compiler.

One thing that you may notice here is that generated method has two parameters (int and String) even though in my lambda I declared only one parameter - String. The reason for this is because in the runtime this method will be called not only with the arguments that I pass (when I execute apply method form Function interface), but also all the "captured" values. Which gets us to point 2:

Lambda expression in runtime

We already know that lambda is converted into actual method, now the question is: what exactly am I getting as the result from the execution of that lamda expression (beside the fact that it's something implementing Function interface)?

The Function<String, String> greeter variable will actually point to an object that internally:

  • has reference to this GreeterFactory object (so that it can later call method on it)
  • holds all (used in the body of lambda expression) "captured" local variables (in my example: value of greeterId)
  • has reference to the generated lambda$createGreeter$0 method

You can see it when you inspect that object in the debugger. Here's what you'll see:

Notice that greeter object has exactly those two values that I mentioned (reference to this GreeterFactory object and a value 23 that was copied from greeterId). That's exactly what "capturing" means in case of lambda expression.

Later when apply is executed on this object, it'll actually call lambda$createGreeter$0 method on the this GreeterFactory object with all captured values + arguments that you pass into apply method.

Back to questions

I hope I already explained above what "capturing" is and how it works. Let's get to point of final/effectively final.

Why captured variables must be effectively final.

disclaimer: I didn't find any official information about it, it's just my assumption, therefore: I may be wrong.

Notice that lambdas exist only on java language level, not on bytecode. Having explained how lambdas work (generation of new method) I think it would be technically possible to capture non-effectively-final variables as well.

I think the reason why designers of lambda expression chose this way is rather focused on helping developers write a bug-free code.

If captured variables where non-effectively-final, meaning: they could be further modified outside of lambda as well as within lambda, that could lead to many confusion and misunderstandings from developers point of view, effectively leading to many bugs. I.e. devs could expect that changing variable's value within lambda should affect this variable in scope of outer method (that's because it's not visible in language that within body of lambda we are actually in scope of that newly generated method), or they could expect the opposite. In short: a total chaos.

I think that's the reason behind such decision and that's why compiler and language enforce it, i.e. by treating lambda's scope and embedding method scope as one (even though in runtime those are different scopes).

Notice that previously the same was true for variables captured by anonymous classes, therefore developers are already familiar with such approach.

Why lambda can freely modify fields in the object? Because it's just a method within the class of this object and as any other method, it has free access to all its members. It would be confusing to expect different behavior.

Answer from Mirek Pluta on Stack Overflow
🌐
Baeldung
baeldung.com › home › cloud › a basic aws lambda example with java
A Basic AWS Lambda Example With Java |Baeldung
April 17, 2025 - AWSTemplateFormatVersion: '2010-09-09' Description: Lambda function deployment with Java 21 runtime Parameters: LambdaHandler: Type: String Description: The handler for the Lambda function S3BucketName: Type: String Description: The name of the S3 bucket containing the Lambda function JAR file S3Key: Type: String Description: The S3 key (file name) of the Lambda function JAR file Resources: BaeldungLambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: baeldung-lambda-function Handler: !Ref LambdaHandler Role: !GetAtt LambdaExecutionRole.Arn Code: S3Bucket: !Ref S3BucketName S3K
🌐
Oracle
docs.oracle.com › javase › tutorial › java › javaOO › lambdaexpressions.html
Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) in Approach 6: Use Standard Functional Interfaces with Lambda Expressions · When the Java runtime invokes the method printPersons, it's expecting a data type of CheckPerson, so the lambda expression is of this type.
🌐
W3Schools
w3schools.com › java › java_lambda.asp
Java Lambda Expressions
The variable's type must be an interface with exactly one method (a functional interface). The lambda must match that method's parameters and return type. Java includes many built-in functional interfaces, such as Consumer (from the java.util package) used with lists.
🌐
Baeldung
baeldung.com › home › lesson 2: functional interfaces and lambdas in java
Lesson 2: Functional Interfaces and Lambdas in Java - Baeldung Membership
October 14, 2025 - 1. Overview Java’s support for functional-style programming begins with two essential features: functional interfaces and lambda expressions. These constructs let us express behavior as values and pass it around with minimal boilerplate - a pattern that’s particularly powerful when working ...
Top answer
1 of 2
8

Capturing of the variable has absolutely nothing to do with the concurrent execution or its safety, the reason is completely different.

Before I answer your questions, let me first explain what is a lambda expression.

What is lambda expression

When you use lambda expression, there are a few things happening, both during compilation and runtime, that are hidden from the developer. It's also worth nothing that lambda expression is part of the java language, it doesn't exist in the generated bytecode.

I'll use following code as an example

public class GreeterFactory {

    private String header = "Hello ";
    
    public Function<String, String> createGreeter(int greeterId){
        Function<String, String> greeter = username -> {
            return String.format("(%s) %s: %s", greeterId, header, username);
        };
        
        return greeter;
    }
}

lamba expression is compiled into anonymous method

When javac compiles java into bytecode, it'll convert your lambda's body into new method in the embedding class (that's why lambda expressions can be though of as anonymous methods).

Here's what will be in the bytecode (decompiled with javap tool):

Compiled from "GreeterFactory.java"
public class various.GreeterFactory {
  private java.lang.String header;
  public various.GreeterFactory();
  public java.util.function.Function<java.lang.String, java.lang.String> createGreeter(int);
  private java.lang.String lambda$createGreeter$0(int, java.lang.String);
}

As you can see the GreeterFactory class not only has the createGreeter method that I've written. It will also now have lambda$createGreeter$0 method that was generated by the compiler.

One thing that you may notice here is that generated method has two parameters (int and String) even though in my lambda I declared only one parameter - String. The reason for this is because in the runtime this method will be called not only with the arguments that I pass (when I execute apply method form Function interface), but also all the "captured" values. Which gets us to point 2:

Lambda expression in runtime

We already know that lambda is converted into actual method, now the question is: what exactly am I getting as the result from the execution of that lamda expression (beside the fact that it's something implementing Function interface)?

The Function<String, String> greeter variable will actually point to an object that internally:

  • has reference to this GreeterFactory object (so that it can later call method on it)
  • holds all (used in the body of lambda expression) "captured" local variables (in my example: value of greeterId)
  • has reference to the generated lambda$createGreeter$0 method

You can see it when you inspect that object in the debugger. Here's what you'll see:

Notice that greeter object has exactly those two values that I mentioned (reference to this GreeterFactory object and a value 23 that was copied from greeterId). That's exactly what "capturing" means in case of lambda expression.

Later when apply is executed on this object, it'll actually call lambda$createGreeter$0 method on the this GreeterFactory object with all captured values + arguments that you pass into apply method.

Back to questions

I hope I already explained above what "capturing" is and how it works. Let's get to point of final/effectively final.

Why captured variables must be effectively final.

disclaimer: I didn't find any official information about it, it's just my assumption, therefore: I may be wrong.

Notice that lambdas exist only on java language level, not on bytecode. Having explained how lambdas work (generation of new method) I think it would be technically possible to capture non-effectively-final variables as well.

I think the reason why designers of lambda expression chose this way is rather focused on helping developers write a bug-free code.

If captured variables where non-effectively-final, meaning: they could be further modified outside of lambda as well as within lambda, that could lead to many confusion and misunderstandings from developers point of view, effectively leading to many bugs. I.e. devs could expect that changing variable's value within lambda should affect this variable in scope of outer method (that's because it's not visible in language that within body of lambda we are actually in scope of that newly generated method), or they could expect the opposite. In short: a total chaos.

I think that's the reason behind such decision and that's why compiler and language enforce it, i.e. by treating lambda's scope and embedding method scope as one (even though in runtime those are different scopes).

Notice that previously the same was true for variables captured by anonymous classes, therefore developers are already familiar with such approach.

Why lambda can freely modify fields in the object? Because it's just a method within the class of this object and as any other method, it has free access to all its members. It would be confusing to expect different behavior.

2 of 2
0

They both exist on the same stack and have a lifecycle together.

No they don't.

Here:

public class OhDearThatWasALieWasntIt {
   void haha() throws Exception {
     var supplier = incrementer(20);
     Thread t = new Thread() {
       public void run() {
         supplier.get();
       }
     }
   }
}

There you go. They don't share a stack at all. Your incrementer local var needs to travel all the way from one thread to an entirely different one, in fact.

The simple fact is, the compiler has no idea where that lambda is going to end up and who shall run it.

Since the stack is allocated for each thread, there can be no concurrency issues.

Baeldung oversimplified, perhaps. If a local var used in a lambda is not final, then there are only 2 options:

[A] the lambda gets a clone and this is incredibly confusing.

[B] the variable is hoisted into heap and we now allow volatile on local vars; the maxim that local vars cannot possibly be shared with other threads is left by the wayside, and concurrency issues abound.

Let's see this in action:

void meanCode() {
  int local = 100;
  Runnable r = () -> {
    for (int i = 0; i < 10; i++) { 
      System.out.println(local++);
    }
  };

  Thread a = new Thread(r);
  a.start();
  Thread.sleep(5);
  for (int i = 0; i < 10; i++) { 
    System.out.println(local++);
  }
}

Either local is now a variable used in 2 places and thus the above code is a race condition, or, a clone is handed out, and both the Runnable and the for loop at the end of the above snippet get their own local copy of local, thus race-condition free, printing 100 through 109 in order, but both print runs arbitrarily interleaved (I guess there's a bit of race condition left). The fact that you secretly have 2 variables is incredibly confusing.

Given that both options are utterly confusing, java instead just doesn't allow it at all. With (effectively) final variables, java gets to just give a copy to the lambda, thus neatly sidestepping any concurrency issues. It's also not confusing, as the variable is (effectively) final.

But there are no threads here!

Yeah you know that. How could the compiler possibly know that? The compiler (and runtime) work on single classes at a time. The compiler isn't going to 'treeshake' your entire project to painstakingly ensure that your code never ends up in a scenario where this stuff ends up in multiple threads. Even if somehow it did, perhaps later on someone recompiles half this code base, or just adds on a few more classes that now do.

🌐
Oracle
oracle.com › webfolder › technetwork › tutorials › obe › java › Lambda-QuickStart › index.html
Java SE 8: Lambda Quick Start
Lambda expressions are a new and important feature included in Java SE 8. They provide a clear and concise way to represent one method interface using an expression. Lambda expressions also improve the Collection libraries making it easier to iterate through, filter, and extract data from a ...
🌐
Baeldung
baeldung.com › home › cloud › writing an enterprise-grade aws lambda in java
Writing an Enterprise-Grade AWS Lambda in Java | Baeldung
June 24, 2025 - In this article, we looked at the importance of features like configuration and logging when using Java to build an enterprise-grade AWS Lambda.
Find elsewhere
🌐
Baeldung
baeldung.com › home › java › core java › functional interfaces in java
Functional Interfaces in Java | Baeldung
March 26, 2025 - This guide focuses on some particular functional interfaces that are present in the java.util.function package. It’s recommended that all functional interfaces have an informative @FunctionalInterface annotation. This clearly communicates the purpose of the interface, and also allows a compiler to generate an error if the annotated interface does not satisfy the conditions. Any interface with a SAM(Single Abstract Method) is a functional interface, and its implementation may be treated as lambda expressions.
🌐
iO Flood
ioflood.com › blog › java-lambda-expressions
Java Lambda Expressions Explained: From Basics to Mastery
February 26, 2024 - Oracle’s Java Tutorials provide a comprehensive guide to lambda expressions in Java. Baeldung’s Guide to Java 8’s Lambdas offers practical tips and best practices for using lambda expressions.
🌐
Baeldung
baeldung.com › home › java › exceptions in java lambda expressions
Exceptions in Java Lambda Expressions | Baeldung
February 25, 2026 - A quick and practical guide to dealing with Lambda Expressions and exceptions
🌐
Baeldung
baeldung.com › home › java › core java › convert anonymous class into lambda in java
Convert Anonymous Class into Lambda in Java | Baeldung
May 29, 2025 - In this article, we learned how to replace an anonymous class with a lambda expression in Java. Along the way, we explained what an anonymous class is and how to convert it into a lambda expression. The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
🌐
Facebook
facebook.com › baeldung › posts › lambda-expressions-and-functional-interfaces-tips-and-best-practices-httpsbuffly › 3277702602466610
Lambda Expressions and Functional Interfaces - Baeldung
Baeldung. 50,426 likes · 373 talking about this. Baeldung publishes in-depth articles and tutorials in the Java ecosystem and general Web Development, with a strong focus on Spring, Spring Security...
🌐
Baeldung
baeldung.com › home › learn java streams & lambdas course
Learn Java Streams & Lambdas Course | Baeldung Courses
January 6, 2026 - In this course, we’ll start by reviewing the core concepts of functional programming, the benefits and downsides of Streams, and how Streams and lambdas work together. Then, we’ll learn how to combine intermediate and terminal operations to process data. We’ll analyze each operation individually, as well as how to chain them effectively, while adhering to the principles of non-interference, statelessness, and non-reusability. Actual coding practice through a Java project – definitely the best way to learn how to work with Streams and lambda expressions
🌐
Baeldung
baeldung.com › home › java › java – powerful comparison with lambdas
Java – Powerful Comparison with Lambdas | Baeldung
January 8, 2024 - In this tutorial, we’re going to take a first look at the Lambda support in Java 8, specifically how to leverage it to write the Comparator and sort a Collection. This article is part of the “Java – Back to Basic” series here on Baeldung.
🌐
Medium
medium.com › swlh › functional-interfaces-and-lambda-expressions-java-8-series-part-1-4cc60f2542ba
Functional Interfaces and Lambda Expressions — Java 8 Series Part 1 | by Pooja Kulkarni | The Startup | Medium
August 10, 2020 - To be a good Java developer, you should be able to understand and implement these concepts. In the next blog, I’ll be explaining a few useful predefined functional interfaces that really come in handy when writing lambda expressions. A few useful references: https://www.baeldung.com/java-8-lambda-expressions-tips ·
🌐
Baeldung
baeldung.com › home › java › java streams › java stream filter with lambda expression
Java Stream Filter with Lambda Expression | Baeldung
January 8, 2024 - Learn how to use lambda expressions with Stream.filter() and handle checked exceptions
🌐
Facebook
facebook.com › baeldung › posts › exceptions-in-java-8-lambda-expressions-from-the-archives › 2289043257999221
Baeldung - Exceptions in Java 8 Lambda Expressions
Baeldung. 50,426 likes · 373 talking about this. Baeldung publishes in-depth articles and tutorials in the Java ecosystem and general Web Development, with a strong focus on Spring, Spring Security...
🌐
ACM Digital Library
dl.acm.org › doi › 10.1145 › 3133909
Understanding the use of lambda expressions in Java | Proceedings of the ACM on Programming Languages
Baeldung. 2016. Lambda Expressions and Functional Interfaces: Tips and Best Practices. (November 2016). http://www. baeldung.com/java-8-lambda-expressions-tips