Kotlin Lazy Properties
Introduction
When building applications in Kotlin, efficient resource management is crucial for performance. One of Kotlin's powerful features is lazy properties, which allow you to delay the initialization of a property until it's actually needed. This can save memory and computational resources, especially for expensive operations.
In this tutorial, you'll learn how lazy properties work in Kotlin, when to use them, and how they can improve your application's performance.
What are Lazy Properties?
A lazy property is a property whose initial value is calculated only when the property is accessed for the first time. After the initial computation, the result is stored and returned directly for subsequent access, without recomputing the value again.
Kotlin provides a standard delegate called lazy()
for implementing lazy properties. Let's look at the basic syntax:
val lazyProperty: Type by lazy { initialization code }
The lazy()
function takes a lambda expression and returns a delegate that implements lazy behavior. The lambda is executed only on the first access to the property.
How Lazy Properties Work
Let's look at a simple example to understand how lazy properties work:
fun main() {
println("Program started")
val regularProperty = computeValue()
val lazyProperty by lazy {
println("Initializing lazy property")
computeValue()
}
println("Regular property is initialized")
println("Accessing regular property: $regularProperty")
println("Lazy property is NOT accessed yet")
println("Now accessing lazy property: $lazyProperty")
println("Accessing lazy property again: $lazyProperty")
}
fun computeValue(): String {
println("Computing value...")
return "Computed Value"
}
Output:
Program started
Computing value...
Regular property is initialized
Accessing regular property: Computed Value
Lazy property is NOT accessed yet
Initializing lazy property
Computing value...
Now accessing lazy property: Computed Value
Accessing lazy property again: Computed Value
Notice how the initialization code for the lazyProperty
is only executed when we first access it. On the second access, the stored value is returned without recomputing.
Thread Safety with Lazy Properties
By default, the lazy()
delegate is thread-safe. This means it guarantees that the initialization lambda will only be invoked once, even if multiple threads are trying to access the property simultaneously.
You can choose from different synchronization modes using the optional mode
parameter:
// Default mode - thread-safe using a lock
val lazyPropertyThreadSafe: String by lazy { "I'm thread safe!" }
// LazyThreadSafetyMode.SYNCHRONIZED - explicitly specifying thread-safe mode (same as default)
val lazyPropertySynchronized: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { "I'm synchronized!" }
// LazyThreadSafetyMode.PUBLICATION - allows multiple initializations but only one gets published
val lazyPropertyPublication: String by lazy(LazyThreadSafetyMode.PUBLICATION) { "I use publication mode!" }
// LazyThreadSafetyMode.NONE - no thread safety guarantees, fastest but use only in single-threaded contexts
val lazyPropertyNone: String by lazy(LazyThreadSafetyMode.NONE) { "No thread safety!" }
Practical Use Cases for Lazy Properties
1. Loading Resource-Intensive Data
Lazy properties are perfect for loading heavy resources only when needed:
class ImageProcessor {
val largeDataset by lazy {
println("Loading large image dataset...")
// Imagine this loads a large dataset from disk
List(10000) { "Image data $it" }
}
fun processFirstImage() {
// Dataset is loaded only when this function is called
println("Processing: ${largeDataset.first()}")
}
fun countImages() {
// Dataset is already loaded, reused without reloading
println("Total images: ${largeDataset.size}")
}
}
fun main() {
val processor = ImageProcessor()
println("ImageProcessor created, dataset not loaded yet")
// Dataset will load here on first access
processor.processFirstImage()
// Dataset already loaded, no loading message appears
processor.countImages()
}
Output:
ImageProcessor created, dataset not loaded yet
Loading large image dataset...
Processing: Image data 0
Total images: 10000
2. Dependency Injection
Lazy properties work well when you need to inject dependencies:
class UserService {
fun getUserDetails(): String = "User details from service"
}
class ProductService {
fun getProductDetails(): String = "Product details from service"
}
class Application {
// Services are created only when actually needed
val userService by lazy {
println("Creating UserService")
UserService()
}
val productService by lazy {
println("Creating ProductService")
ProductService()
}
fun processUserRequest() {
println(userService.getUserDetails())
}
fun processProductRequest() {
println(productService.getProductDetails())
}
}
fun main() {
val app = Application()
println("Application started")
app.processUserRequest() // UserService created here
app.processUserRequest() // UserService reused
app.processProductRequest() // ProductService created here
}
Output:
Application started
Creating UserService
User details from service
User details from service
Creating ProductService
Product details from service
3. Configuration Settings
Lazy properties are great for loading configuration settings:
class AppConfig {
val databaseSettings by lazy {
println("Loading database settings from config file...")
mapOf(
"url" to "jdbc:mysql://localhost:3306/mydb",
"username" to "user",
"password" to "pass"
)
}
val loggingSettings by lazy {
println("Loading logging settings from config file...")
mapOf(
"level" to "INFO",
"path" to "/var/log/app.log"
)
}
}
fun main() {
val config = AppConfig()
// Only database settings are loaded, not logging settings
println("DB URL: ${config.databaseSettings["url"]}")
println("DB User: ${config.databaseSettings["username"]}")
// Now logging settings are loaded
println("Log Level: ${config.loggingSettings["level"]}")
}
Output:
Loading database settings from config file...
DB URL: jdbc:mysql://localhost:3306/mydb
DB User: user
Loading logging settings from config file...
Log Level: INFO
Lazy Properties vs. lateinit
Kotlin offers two ways to delay initialization: lazy
properties and lateinit
variables. Here's how they compare:
Feature | lazy properties | lateinit variables |
---|---|---|
Initialization | Automatic on first access | Manual, must be initialized before first use |
Property type | val (read-only) | var (mutable) |
Nullability | Non-null types | Non-null types |
Value caching | Yes, computed once | No, can be changed |
Thread safety | Yes (by default) | No |
Primitive types | Supported | Not supported |
Use case | When value calculation is expensive | When initialization happens in another method |
Example of lateinit
:
class UserController {
// Will be initialized later, not on property declaration
lateinit var userService: UserService
fun initialize() {
userService = UserService()
}
fun getUser() {
// Will throw UninitializedPropertyAccessException if initialize() wasn't called
return userService.getUserDetails()
}
}
Common Pitfalls
-
Cyclic dependencies: Be cautious with lazy properties that depend on each other, as this can lead to initialization issues.
-
Exception handling: If the initialization lambda throws an exception, it will be thrown again on each property access attempt.
val problematicLazy by lazy {
println("Attempting initialization...")
throw RuntimeException("Initialization failed")
}
fun main() {
try {
println(problematicLazy)
} catch (e: Exception) {
println("First access failed: ${e.message}")
}
try {
println(problematicLazy) // Will throw exception again
} catch (e: Exception) {
println("Second access failed: ${e.message}")
}
}
Output:
Attempting initialization...
First access failed: Initialization failed
Attempting initialization...
Second access failed: Initialization failed
- Memory leaks: Lazy properties hold strong references to objects. In classes with long lifecycles, this could lead to memory leaks if the lazy property references objects that should be garbage collected.
Summary
Kotlin's lazy properties are a powerful tool that allows you to:
- Delay initialization of properties until they're actually needed
- Improve application startup performance by deferring resource-intensive operations
- Cache computed values for subsequent access
- Maintain thread safety (by default)
Use lazy properties when:
- Initialization is computationally expensive
- The property might not be used in all execution paths
- You need thread-safe initialization with caching
Exercises
-
Create a class that uses lazy properties to load different sections of a configuration file.
-
Implement a caching system using lazy properties that loads data from a "database" (you can simulate this with a function that prints a loading message).
-
Create a program that demonstrates the thread safety of lazy properties by accessing the same property from multiple threads.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)