Kotlin is a modern, statically typed programming language that runs on the Java Virtual Machine (JVM). One of its key features is support for lambda expressions, which provide a concise and expressive way to define functions inline. In Kotlin, lambdas can capture local variables, which allows them to extend the scope of those variables beyond the function in which they are declared. This feature is extremely powerful, but it can also be somewhat confusing if you’re not familiar with how variable capturing works. In this article, we’ll explore the topic of variable capturing in Kotlin lambdas in-depth, with plenty of examples along the way.
What is Variable Capturing?
In Kotlin, the lifetime of a local variable is determined by the function in which it is declared. This means that the variable can only be accessed within that function and will be destroyed once the function finishes executing.
However, if a local variable is captured by a lambda expression, the variable’s scope can be extended beyond the function in which it was declared. This means that the code that uses the variable can be stored and executed later.
If the variable is declared as final, its value is stored together with the lambda code that uses it. This is because the value of a final variable cannot be changed once it has been assigned.
On the other hand, if the variable is not final, its value is enclosed in a special wrapper that allows you to change it. The reference to this wrapper is then stored together with the lambda, so that the lambda can access and modify the value of the variable even after the function in which it was declared has finished executing.
This behavior is called “capturing” a variable, and it is a powerful feature of Kotlin’s lambda expressions that allows for more flexible and expressive programming.
Examples
Let’s dive into some code examples to better understand how local variables are captured by lambdas in Kotlin.
First, let’s define a simple function that takes an integer argument and returns a lambda that multiplies its input by a factor:
fun multiplyBy(factor: Int): (Int) -> Int {
return { input: Int -> input * factor }
}
In this example, the function multiplyBy
returns a lambda that captures the factor
variable. When the lambda is executed, it multiplies its input parameter by factor
and returns the result.
We can use this function to create two lambdas that multiply their input by different factors:
val double = multiplyBy(2)
val triple = multiplyBy(3)
Here, we’re creating two new lambdas by calling multiplyBy
with different values for factor
. double
captures the value 2
, while triple
captures the value 3
.
Now, we can use these lambdas to perform some calculations:
val result1 = double(5) // returns 10
val result2 = triple(5) // returns 15
Here, we’re calling double
and triple
with the input value 5
. double(5)
returns 10
, because 2 * 5 = 10
. triple(5)
returns 15
, because 3 * 5 = 15
.
Notice that even though double
and triple
capture the value of factor
when they are created, they can be executed with different input values later. This is because the captured factor
variable is stored along with the lambda code, and can be used each time the lambda is executed.
Now, let’s look at an example of capturing a non-final variable. Consider the following function:
fun counter(): () -> Int {
var count = 0
return {
count++
count
}
}
This function returns a lambda that increments and returns a local variable count
each time it is executed. The count
variable is not declared as final
, which means that its value can be changed.
We can use this function to create two lambdas that count the number of times they are executed:
val increment1 = counter()
val increment2 = counter()
Here, we’re creating two new lambdas by calling counter
twice. increment1
and increment2
both capture the same count
variable.
Now, let’s execute these lambdas and see what happens:
val result1 = increment1() // returns 1
val result2 = increment2() // returns 1
val result3 = increment1() // returns 2
val result4 = increment2() // returns 2
Here, we’re calling increment1
and increment2
multiple times. The first time each lambda is called, it returns 1
, because the initial value of count
is 0
. The second time each lambda is called, it returns 2
, because the value of count
has been incremented once.
Notice that both increment1
and increment2
are accessing the same count
variable, and that the value of count
is being modified each time the lambdas are executed. This is possible because Kotlin creates a special wrapper object for non-final captured variables that allows their values to be modified by the lambda.
Final Variables
When a lambda captures a local variable in Kotlin, the captured variable must be either val
or final
in Java terminology. This means that the variable must be immutable, or effectively immutable, by the time the lambda captures it. If the variable is mutable, the behavior of the lambda can be unpredictable.
Here’s an example that demonstrates capturing a final variable in Kotlin:
fun outerFunction(): () -> Unit {
val message = "Hello, softAai!"
return { println(message) }
}
val lambda = outerFunction()
lambda() // prints "Hello, softAai!"
In this example, the outerFunction
returns a lambda that captures the final variable message
. The lambda prints the value of message
when it’s executed. The value of message
cannot be modified, so this lambda will always print “Hello, softAai!”.
Non-Final Variables
When a lambda captures a non-final variable in Kotlin, the variable is effectively wrapped in an object that can be modified by the lambda. This allows the lambda to modify the value of the variable even after it has been captured. However, there are some important rules to keep in mind when capturing non-final variables.
Here’s an example that demonstrates capturing a non-final variable in Kotlin:
fun outerFunction(): () -> Unit {
var counter = 0
return { println(counter++) }
}
val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"
In this example, the outerFunction
returns a lambda that captures the non-final variable counter
. The lambda prints the value of counter
when it’s executed and increments it by one. The value of counter
can be modified by the lambda, so each time the lambda is executed, the value of counter
will increase.
However, if you try to modify a captured variable from outside the lambda, you’ll get a compilation error:
fun outerFunction(): () -> Unit {
var counter = 0
return { println(counter++) }
}
val lambda = outerFunction()
lambda.counter = 10 // Compilation error: "Unresolved reference: counter"
This is because the captured variable is effectively wrapped in an object that can only be accessed and modified by the lambda itself. If you want to modify the value of the captured variable from outside the lambda, you’ll need to create a separate variable and update it manually:
fun outerFunction(): () -> Unit {
var counter = 0
return {
val newCounter = 10
println(newCounter)
}
}
val lambda = outerFunction()
lambda() // prints "10"
In this example, we’ve created a new variable called newCounter
inside the lambda and assigned it a value of 10. This allows us to modify the value of the variable without modifying the captured variable.
Capturing mutable variables
The concept of capturing mutable variables in Kotlin lambdas may be a bit confusing, especially for those coming from Java, where only final variables can be captured.
In Kotlin, you can use a trick to capture mutable variables by either declaring an array of one element in which to store the mutable value, or by creating an instance of a wrapper class that stores the reference that can be changed.
To illustrate this, you can create a Ref
class with a mutable value
property, which can be used to capture a mutable variable in a lambda. Here’s an example of how this can be done:
class Ref<T>(var value: T)
val counter = Ref(0)
val inc = { counter.value++ }
In this example, a counter
variable of type Ref
is created with an initial value of 0, and a lambda expression inc
is defined to increment the value
property of the counter
object each time it’s called.
By using the Ref
class, you are simulating the capturing of a mutable variable in a lambda, by actually capturing an immutable reference to an instance of the Ref
class, which can be mutated to change the value of its value
property.
So in Kotlin, you can directly capture a mutable variable like a var
by simply referencing it within the lambda. This is because, under the hood, Kotlin creates an instance of a Ref
class to capture the mutable variable, and any changes made to it are reflected in the original variable outside the lambda.
Here’s an example:
var counter = 0
val inc = { counter++ }
In this example, a counter
variable is declared as a var
with an initial value of 0, and a lambda expression inc
is defined to increment the counter
variable each time it’s called.
As here mentioned, the first example with the Ref
class shows how the second example works under the hood. When you capture a final variable (val
), its value is copied, similar to how it works in Java. However, when you capture a mutable variable (var
), Kotlin creates an instance of a Ref
class to store the value of the mutable variable, which is then captured as a final variable. The actual value of the mutable variable is then stored in a field of the Ref
class, which can be changed from the lambda.
Capturing Objects
When a lambda captures an object in Kotlin, it captures a reference to the object rather than a copy of the object itself. This means that if you modify the object outside the lambda, the changes will be visible inside the lambda.
Here’s an example that demonstrates capturing an object in Kotlin:
class Counter {
var value = 0
}
fun outerFunction(): () -> Unit {
val counter = Counter()
return { println(counter.value++) }
}
val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"
In this example, we’ve defined a simple Counter
class with a single value
property. We’ve also defined an outerFunction
that creates a new Counter
object and returns a lambda that captures the object. The lambda prints the value of the value
property when it’s executed and increments it by one.
If you modify the value
property of the Counter
object outside the lambda, the changes will be visible inside the lambda:
class Counter {
var value = 0
}
fun outerFunction(): () -> Unit {
val counter = Counter()
return { println(counter.value++) }
}
val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"
lambda() // prints "2"
lambda() // prints "3"
val counter = Counter()
counter.value = 10
lambda() // prints "4"
In this example, we’ve created a new Counter
object called counter
and set its value
property to 10. When we call the lambda again, it prints “4”, which shows that the changes to the Counter
object are visible inside the lambda as here we deal with another object so changes won’t reflect.
Let’s take another example to understand it clearly.
data class Person(val name: String, var age: Int)
fun main() {
var person = Person("Alice", 30)
val incrementAge = { person.age += 1 }
println(person) // Output: Person(name=Alice, age=30)
incrementAge()
println(person) // Output: Person(name=Alice, age=31)
person.age += 1
println(person) // Output: Person(name=Alice, age=32)
incrementAge()
println(person) // Output: Person(name=Alice, age=33)
}
In this example, we have a Person
class with a name
and an age
property. We also have a lambda expression incrementAge
that captures the person
object and increments its age
property by 1.
When we execute the program, we first print the person
object, which has an age
of 30. We then execute the incrementAge
lambda expression, which modifies the age
property of the person
object to 31. We print the person
object again and see that its age
property has been updated to 31.
After that, we modify the age
property of the person
object outside of the lambda expression, by incrementing it by 1. We print the person
object again and see that its age
property has been updated to 32.
Finally, we execute the incrementAge
lambda expression again, which modifies the age
property of the person
object to 33. We print the person
object one last time and see that its age
property has been updated to 33.
What’s happening here is that when we define the incrementAge
lambda expression, it captures a reference to the person
object, not a copy of it. This means that when we execute the lambda expression and modify the age
property of the person
object, we are modifying the same object that exists outside of the lambda expression.
So, when we modify the age
property of the person
object outside of the lambda expression, those changes are visible inside the lambda expression because they are happening to the same object that the lambda expression has captured a reference to.
Conclusion
Capturing variables and objects in Kotlin lambdas can be a powerful tool for writing concise and expressive code. By understanding the rules for capturing final and non-final variables and objects, you can write code that behaves exactly as you expect. However, it’s important to be careful when capturing variables and objects, especially when working with mutable state. By following these guidelines, you can write safe and effective Kotlin code that uses lambdas to their full potential.