
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 delegate[^032_1]:val userRepo by lazy { UserRepository() } // Alternative code not using lazy private var _userRepo: UserRepository? = null private val userRepoLock = Any() val userRepo: UserRepository get() { synchronized(userRepoLock) { if (_userRepo == null) { _userRepo = UserRepository() } return _userRepo!! } }
A, which composes classes B, C, and 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.class A { val b = B() val c = C() val d = D() // ... }
A lighter by making b, c, and 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.class A { val b by lazy { B() } val c by lazy { C() } val d by lazy { D() } // ... }
A might be FlashcardsParser, which is used to parse files defined in a language we have invented; B, C, and D might be some complex regex parsers which are heavy to initialize.class OurLanguageParser { val cardRegex by lazy { Regex("...") } val questionRegex by lazy { Regex("...") } val answerRegex by lazy { Regex("...") } // ... }
fun String.isValidIpAddress(): Boolean { return this.matches( ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() ) } // Usage print("5.173.80.254".isValidIpAddress()) // true
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:private val IS_VALID_IP_REGEX = ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() fun String.isValidIpAddress(): Boolean = matches(IS_VALID_IP_REGEX)
IS_VALID_IP_REGEX property lazy.private val IS_VALID_IP_REGEX by lazy { ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() } fun String.isValidIpAddress(): Boolean = matches(IS_VALID_IP_REGEX)
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.data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String = produceFullDisplay() fun produceFullDisplay(): String { println("Calculating...") // ... return "XYZ" } } fun test() { val user = User(...) // Calculating... val copy = user.copy() // Calculating... println(copy.fullDisplay) // XYZ println(copy.fullDisplay) // XYZ }
fullDisplay property using a getter, it will be re-calculated whenever it is used, which also might be an unnecessary cost.data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String get() = produceFullDisplay() fun produceFullDisplay(): String { println("Calculating...") // ... return "XYZ" } } fun test() { val user = User(...) val copy = user.copy() println(copy.fullDisplay) // Calculating... XYZ println(copy.fullDisplay) // Calculating... XYZ }
fullDisplay property as lazy is a sweet spot for properties that:- are read-only (
lazycan only be used forval), - 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).
data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String by lazy { produceFullDisplay() } fun produceFullDisplay() { println("Calculating...") // ... } } fun test() { val user = User(...) val copy = user.copy() println(copy.fullDisplay) // Calculating... XYZ println(copy.fullDisplay) // XYZ }
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 thanSYNCHRONIZED; 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.
val v1 by lazy { calculate() } val v2 by lazy(LazyThreadSafetyMode.PUBLICATION){ calculate() } val v3 by lazy(LazyThreadSafetyMode.NONE) { calculate() }
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.class MainActivity : Activity() { lateinit var questionLabelView: TextView lateinit var answerLabelView: EditText lateinit var confirmButtonView: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) questionLabelView = findViewById(R.id.main_question_label) answerLabelView = findViewById(R.id.main_answer_label) confirmButtonView = findViewById(R.id.main_button_confirm) } }
class MainActivity : Activity() { val questionLabelView: TextView by lazy { findViewById(R.id.main_question_label) } val answerLabelView: TextView by lazy { findViewById(R.id.main_answer_label) } val confirmButtonView: Button by lazy { findViewById(R.id.main_button_confirm) } override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } }
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.bindView, and it helped us make our view references really clear, consistent, and readable.class MainActivity : Activity() { var questionLabelView: TextView by bindView(R.id.main_question_label) var answerLabelView: TextView by bindView(R.id.main_answer_label) var confirmButtonView: Button by bindView(R.id.main_button_confirm) override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } } // ActivityExt.kt fun <T : View> Activity.bindView(viewId: Int) = lazy { this.findViewById<T>(viewId) }
class SettingsActivity : Activity() { private val titleString by bindString(R.string.title) private val blueColor by bindColor(R.color.blue) private val doctor by extra<Doctor>(DOCTOR_KEY) private val title by extraString(TITLE_KEY) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) } } // ActivityExt.kt fun <T> Activity.bindString(@IdRes id: Int): Lazy<T> = lazy { this.getString(id) } fun <T> Activity.bindColor(@IdRes id: Int): Lazy<T> = lazy { this.getColour(id) } fun <T : Parcelable> Activity.extra(key: String) = lazy { this.intent.extras.getParcelable(key) } fun Activity.extraString(key: String) = lazy { this.intent.extras.getString(key) }
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 code[^032_2]:class Lazy { var x = 0 val y by lazy { 1 / x } fun hello() { try { print(y) } catch (e: Exception) { x = 1 print(y) } } } fun main(args: Array<String>) { Lazy().hello() }
- 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 ofx; when we call the lambda expression for the second time,xis 1, so the result should be 1.
lazy implementation is more complicated as it has better synchronization, which secures it for concurrent use.[^032_2]: 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.
[^032_3]: By computed property, I mean a property whose value is determined by other properties and therefore can always be recalculated.
[^032_4]: Also known as memoization.
