1. Overview
In this tutorial we’ll see what it means to use an open member in a Kotlin constructor, why we would use it and the common pitfalls.
We’ll discuss the pros and cons of using it and finally explore how Kotlin provides an alternative with lazy initialization.
2. Using an Open Member in a Kotlin Constructor
To understand how an open member is used in a Kotlin constructor, let’s first create the classes Language, English and Spanish:
abstract class Language {
val alphabetSize = calculateAlphabetSize()
abstract fun calculateAlphabetSize(): Int
}
class English(val alphabet: String = "abcdefghijklmnopqrstuvwxyz") : Language() {
override fun calculateAlphabetSize() = alphabet.length
}
class Spanish(val alphabet: String = "abcdefghijklmnñopqrstuvwxyz") : Language() {
override fun calculateAlphabetSize() = alphabet.length
}
We see that the Kotlin compiler gives a warning when declaring alphabetSize:
Calling non-final function calculateAlphabetSize in constructor
This is considered a constructor call owing to the way classes are initialized in Kotlin.
To understand why this warning exists, let’s try to execute some unit tests based on what we’d expect from the code:
@Test
fun `Languages should return their alphabet sizes`() {
val languageA: Language = English()
val languageB: Language = Spanish()
assertEquals(26, languageA.alphabetSize)
assertEquals(27, languageB.alphabetSize)
}
The assertions will fail! alphabetSize returns null, because when we call the calculateAlphabetSize() method, the implementations weren’t initialized yet, and thus we won’t get it in the correct order. null is returned as it’s the default primitive value in the JVM, where we are running our code.
3. Pros and Cons and Good Practice
With this approach, we are trying to use a method from a partially constructed object. It might not be ready for our call, and thus the compiler complains and depending on the code, the behavior may not be as intended. Indeed, our tests here fail as a result. Using an open member before the object is fully initialized is dangerous.
The greatest advantage of this approach is how quick we can get access to the implementing class’ details. We can use a more specific definition from the parent class, which can lead to code that’s more intuitive to read and maintain.
In our scenario, we could add code that’s independent of initialization and would behave as expected. Let’s see an example of that using the alphabet’s name:
abstract class Language {
val alphabetNameSize = calculateAlphabetNameSize()
abstract fun calculateAlphabetNameSize(): Int
}
class English(val alphabet: String = "abcdefghijklmnopqrstuvwxyz") : Language() {
override fun calculateAlphabetSize() = alphabet.length
override fun calculateAlphabetNameSize() = "English".length
}
class Spanish(val alphabet: String = "abcdefghijklmnñopqrstuvwxyz") : Language() {
override fun calculateAlphabetSize() = alphabet.length
override fun calculateAlphabetNameSize() = "Spanish".length
}
@Test
fun `Languages should return their alphabet name sizes`() {
val languageA: Language = English()
val languageB: Language = Spanish()
assertEquals(7, languageA.alphabetNameSize)
assertEquals(7, languageB.alphabetNameSize)
}
To avoid the common pitfalls, we usually approach this problem by doing lazy initialization.
To initialize a variable lazily is to compute its value only when we actually use it. This lets us reference the subclass in the parent class, but not doing the computation until everything is ready.
4. Use Kotlin Lazy Initializer
Kotlin gives us a a delegated property to handle this and other use cases. By delegating our computation to the lazy property we can avoid the danger of partially constructed objects.
Let’s see how we could adapt our code to use the lazy delegate:
abstract class Language {
val alphabetSize by lazy { calculateAlphabetSize() }
abstract fun calculateAlphabetSize(): Int
}
If we now run the test we implemented earlier we’ll see that it passes! The lazy function is only called when our test runs, and at that time the class is ready and fully constructed.
5. Conclusion
In this article, we understood why we should avoid using open members when constructing an object.
We looked at an example of the problem, and learned how to solve it by using lazy initialization.
As always, the code for this can be found over on GitHub.