Lazy property delegate
The most popular property delegate is
lazy. It implements the lazy property pattern, so it postpones read-only value initialization until this value is needed for the first time. This is what an example usage of
lazy looks like, and this is what we would need to implement if we didn’t have this delegate1:
The lazy delegate is often used as a performance optimization for objects that are expensive to calculate. Let's start with an abstract example. Consider class
A, which composes classes
D, each of which is heavy to initialize. This makes the instance of
A really heavy to initialize because it requires the initialization of multiple heavy objects.
We can make the initialization of
A lighter by making
d lazy properties. Thanks to that, initialization of their values is postponed until their first use; if these properties are never used, the associated class instances will never be initialized. Such an operation improves class creation time, which benefits application startup time and test execution time.
To make this example more practical,
A might be
FlashcardsParser, which is used to parse files defined in a language we have invented;
D might be some complex regex parsers which are heavy to initialize.
Regex is a really useful notation when we need to process text. However, regex definitions can be very complex, and their parsing is a heavy operation. This reminds me of another example I have famously shown in the Effective Kotlin book. Consider a function in which we use regex to determine whether a string contains a valid IP address:
The problem with this function is that the
Regex object needs to be created every time we use it. This is a serious disadvantage since regex pattern compilation is complex, therefore this function is unsuitable for repeated use in performance-constrained parts of our code. However, we can improve it by lifting the regex up to the top level:
The problem now is that this regex is initialized whenever we initialize this file for the first time; so, if this file contains more elements (like other functions or properties), this regex might slow down the usage of these elements for no good reason. This is why it is better to make the
IS_VALID_IP_REGEX property lazy.
This technique4 can be used in a variety of situations. Consider a data class that stores a computed property3 that is not trivial to calculate. For example, consider the
User class with the
fullDisplay property, which is calculated from other properties but includes some significant logic as it depends on multiple properties and project configurations. If we define
fullDisplay as a regular property, it will be calculated whenever a new instance of
User is created, which might be an unnecessary cost.
If we define the
fullDisplay property using a getter, it will be re-calculated whenever it is used, which also might be an unnecessary cost.
fullDisplay property as lazy is a sweet spot for properties that:
- are read-only (
lazycan only be used for
- are non-trivial to calculate (otherwise, using
lazyis not worth the effort),
- might not be used for all instances (otherwise, use a regular property),
- might be used more than once by one instance (otherwise, define a property with a getter).
When we consider using a
lazy delegate as a performance optimization, we should also consider which thread safety mode we want it to use. It can be specified in an additional
mode function argument, which accepts the enum
LazyThreadSafetyMode. The following options are available:
SYNCHRONIZEDis the default and safest option and uses locks to ensure that only a single thread can initialize this delegate instance. This option is also the slowest because synchronization mechanisms introduce some performance costs.
PUBLICATIONmeans that the initializer function can be called several times on concurrent access to an uninitialized delegate instance value, but only the first returned value will be used as the value of this delegate instance. If a delegate is used only by a single thread, this option will be slightly faster than
SYNCHRONIZED; however, if a delegate is used by multiple threads, we need to cover the possible cost of recalculation of the same value before the value is initialized.
NONEis the fastest option and uses no locks to synchronize access to the delegate instance value; so, if the instance is accessed from multiple threads, its behavior is undefined. This mode should not be used unless this instance is guaranteed to never be initialized from more than one thread.
In all these examples, we used
lazy as a performance optimization, but there are other reasons to use it. In this context, I’d like to share a story from my early days of using Kotlin on Android. It was around 2015. Kotlin was still before the stable release, and the Kotlin community was inventing ways of using its features to help us with everyday tasks. You see, in Android we have a concept of Activity, which is like a window that defines its view, traditionally using XML files. Then, the class representing an Activity often modifies this view by programmatically changing text or some of its properties. To change a particular view element, we need to reference it, but we cannot reference it before the view is set with the
setContentView function. Back then, this is why it was standard practice to define references to view elements as lateinit properties and associate appropriate view elements with them immediately after setting up the content view.
This pattern was used in nearly all Android applications (and I can still see it nowadays in some projects), even though it is far from perfect. We need to define multiple lateinit properties, which are all read-write even though they are initialized only once. Property definition and assignment are separated. If a property is not used, it will not be marked by the IDE because an assignment is considered a usage. Overall, there is a lot of space for improvement. The solution turned out to be extremely simple: we can define property initialization next to its definition if we make it lazy.
Thanks to the
lazy delegate, finding the view by id will be postponed until the property is used for the first time, which we can assume will happen after the content view is set. We made such an assumption in the previous solution, so why not make it in this one too? We also have some important improvements: properties that keep view references are read-only, initialization and definition are kept together, and unused properties will be marked. We also have performance benefits: a view reference that is never used will never be associated with the view element, which is good because the
findViewById execution can be expensive.
What is more, we can push this pattern further and extract a function that will both define a lazy delegate and find a view by id. In a project I co-created, we named this delegate
bindView, and it helped us make our view references really clear, consistent, and readable.
We started using this pattern to bind other references as well, like strings or colors. But my favorite part was binding Activity arguments, which I still use today in some of my projects because it’s very convenient to have all activity properties defined together with their keys at the top of this activity definition.
In the backend application, you can also find lazy delegates that are used to initialize properties that should be initialized when they are needed for the first time, but not during project setup. A simple example is a connection to a local database.
As you can see,
lazy is a powerful delegate that is mainly used for performance optimization but can also be used to simplify our code. I would like to finish this section with a small puzzle. Consider the following code2:
What will be printed? Try to answer this question before reading any further.
To understand the answer, we need to consider three things:
- Delegate exceptions are propagated out of accessors.
- A lazy delegate first tries to return a previously calculated value; if no value is already stored, it uses a lambda expression to calculate it. If the calculation process is disturbed with an exception, no value is stored; when we ask for the lazy value the next time, processing starts again.
- Kotlin's lambda expressions automatically capture variable references; so, when we use
xinside a lambda expression, every time we use its reference inside a lambda expression we will receive the current value of
x; when we call the lambda expression for the second time,
xis 1, so the result should be 1.
So, the answer to this puzzle is "1".
In the next part of this series, I will show you the amazing observable and vetoable property delegates. So, stay tuned!
lazy implementation is more complicated as it has better synchronization, which secures it for concurrent use.
I first heard about this puzzle from Anton Keks' presentation and it can be found in his repository, so I assume he is the author.
By computed property, I mean a property whose value is determined by other properties and therefore can always be recalculated.
Also known as memoization.