Skip to Content

Getting something out of Kotlin

Properties vs. get() vs. lazy delegates

Posted on
Photo by Luke Paris on Unsplash
Photo by Luke Paris on Unsplash

A couple of weeks ago, I answered a question on Stack Overflow that asked the difference between accessing a value via get() and a lazy delegate. I wrote up an answer that was well received, and then ran into the same situation a few days later at work during a code read. So I did some thinking about it and wanted to expand on my answer and figure out when each of these concepts is best used.

Evaluation Criteria

When evaluating our options, there are two factors we will be looking at - when do values get calculated, and how often? As you’ll see by the end of this post, there are different options available depending on the use case.

Common Code

In order to ease our evaluation, we will use an AtomicInteger to generate numbers for us. I’ve added an extension function to AtomicInteger which will allow us to get the next number and print it in one call. This setup lets us see the sequence of calculations, and how often they happen.

fun AtomicInteger.next(): Int = 
    this.incrementAndGet().also {
        println("Generating: $it")
    }

Default Property Accessor

First up is the most commonly seen, a property with its default accessor. In this example, we will use an AtomicInteer to set the initial value of our property. This will allow us to trace when it is calculated and how often it refreshes (if at all!).

class PropertyAccessor(generator: AtomicInteger = AtomicInteger()) {
    val theNumberProperty: Int = generator.next()
}

And then, we’ll instantiate our PropertyAccessor class and see what happens:

fun main() {
    println("Creating PropertyAccessor")
    val pa = PropertyAccessor()
    println("Created PropertyAccessor")

    // Access our property a few times:
    repeat(3) {
        println("About to get")
        println("Got: ${pa.theNumberProperty}")
    }
}
// Output:
Creating PropertyAccessor
Generating: 1
Created PropertyAccessor
About to get
Got: 1
About to get
Got: 1
About to get
Got: 1

As we can see, the property value (theNumberProperty) is only calculated when the PropertyAccessor object is instantiated, and it is not refreshed on any subsequent access.

Overriding get() on a Property

In Kotlin, if we don’t like the default behavior for a property getter or setter, we can override them. In this case, because we are only dealing with val properties, we don’t have a setter. So we’ll override the getter and have it call through to our AtomicInteger again. This time, we should see different results.

class OverriddenAccessor(private val generator: AtomicInteger = AtomicInteger()) {
    val theNumberProperty: Int 
        get() = generator.next()
}

And as above, we’ll instantiate our OverriddenAccessor class and access our property a few times:

fun main() {
    println("Creating OverriddenAccessor")
    val oa = OverriddenAccessor()
    println("Created OverriddenAccessor")

    // Access our property a few times:
    repeat(3) {
        println("About to get")
        println("Got: ${oa.theNumberProperty}")
    }
}
// Output:
Creating OverriddenAccessor
Created OverriddenAccessor
About to get
Generating: 1
Got: 1
About to get
Generating: 2
Got: 2
About to get
Generating: 3
Got: 3

Well, that’s different! In contrast to above, our property value doesn’t get created until we access it for the first time. Another major difference is that the value is re-calculated every time we access it! Granted, we can write out get() function any way we want here, so there are quite a few more variants we could go with, but this seems to be the most direct way of illustrating the point.

The Lazy Delegate

The final approach we will look at involves the Lazy Property Delegate. By delegating our property to lazy, we can defer the calculation of our value until we actually need it (if ever). This is very nice because we can avoid doing work we might not end up needing. As above, we will write a class with a single property, this time backed by lazy:

class LazyDelegateAccessor(private val generator: AtomicInteger = AtomicInteger()) { 
    val theNumberProperty: Int by lazy {
        generator.next()
    }
}

Our testing approach should look familiar at this point:

fun main() {
    println("Creating LazyDelegateAccessor")
    val lda = LazyDelegateAccessor()
    println("Created LazyDelegateAccessor")

    // Access our property a few times:
    repeat(3) {
        println("About to get")
        println("Got: ${lda.theNumberProperty}")
    }
}
// Output:
Creating LazyDelegateAccessor
Created LazyDelegateAccessor
About to get
Generating: 1
Got: 1
About to get
Got: 1
About to get
Got: 1

This looks like a combination of the approaches above! Our value is only calculated when it is first accessed (like overriding get()) but is not recalculated on subsequent access (like the default property implementation). The lazy delegate will remember the answer it originally calculated and return that if we ask for it again.

Summary

The table below provides a summary indicating which type of construct to use depending on when values should be created and how often.

Concept When first calculated Calculated how often?
Default Property Accessor Object instantiation Once
Overridden Property Accessor First access Every access
Lazy Delegate First access Once

There is one missing combination here - a value which is calculated when the object is instantiated but changes every time the value is accessed. We could probably do this by implementing our own property delegate, but I can’t think of a use case where I would actually want this behavior.