Properties
In the chapter on classes, we saw that we can have properties in our Kotlin classes, reminiscent of having fields in Java:
class Foo {
var count = 0
fun something() {
count += 1
println("something() was called $count times")
}
}
fun main() {
val foo = Foo()
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
In a nutshell, you can have var
- and val
-labeled properties in your classes, just like you can outside of classes. Properties in classes are accessible by instances of those classes, just as the something()
function in Foo
has access to the count
property.
So far, this is unremarkable, and you might wonder why there is a whole chapter dedicated to properties. That is because properties in Kotlin have a number of features that go beyond their equivalents in other popular programming languages. These features are commonly used in Kotlin development, and so it is important to understand what they are and how you use them.
Initialization
Properties need to be initialized at some point. Kotlin gives you some common options, and some less-common options, for doing this.
When Declared
Much of the time, you will initialize properties when you declare them. Primarily, such initialization will refer to literals, constructor parameters, properties declared outside of classes, and perhaps constants. They can also refer to other properties that themselves are initialized.
Here, we initialize count
based on a sillyCount
constructor parameter:
class Foo(sillyCount: String) {
var count = sillyCount.toInt()
fun something() {
count += 1
println("something() was not called $count times")
}
}
fun main() {
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
In an Init Block
You can also initialize properties in an init
block. In the chapter on classes, we saw this example:
class Foo(val sillyCount: String) {
var count = 0
init {
count = sillyCount.toInt()
}
fun something() {
count += 1
println("something() was not called $count times")
}
}
fun main() {
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, we initialize count
to 0
, then replace it immediately with a value derived from sillyCount
. As it turns out, the initialization is not required. This is just as valid and gives the same results:
class Foo(sillyCount: String) {
var count: Int
init {
count = sillyCount.toInt()
}
fun something() {
count += 1
println("something() was called $count times")
}
}
fun main() {
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Sometime Later
It may be that you do not know how to initialize the property now, but you will be able to initialize it later. For example, in Android app development, there will be properties that you cannot initialize until some particular point in an object’s lifecycle, such as in onCreate()
of an Activity
or Fragment
.
The most common way of handling this is via lateinit
. lateinit
is a promise that you will not attempt to reference a property until you initialize it “late” (i.e., sometime after the object is initialized):
class Foo {
var count = 0
lateinit var label: String
fun defineTag(tag: String) {
label = tag
}
fun something() {
count += 1
println("$label: something() was called $count times")
}
}
fun main() {
val foo = Foo()
foo.defineTag("Foo")
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, we have both a count
and a label
, where we use both in the printed output. For some reason, we do not have the value for label
at the time we create an instance of Foo
. So, we define label
as lateinit
, so we can skip the initialization.
The implied requirement is that defineTag()
must be called before something()
. We need to initialize label
before we can refer to it safely. Failing to do this — such as by commenting out the foo.defineTag("Foo")
line in the sample — results in a crash:
kotlin.UninitializedPropertyAccessException: lateinit property label has not been initialized
Where possible, you should try to avoid lateinit
, as it is easy to make mistakes and try using the lateinit
property before it is initialized.
Also note that lateinit
cannot be used for types based on primitives, so you cannot have a lateinit
Int
, Boolean
, etc.
Eh, Whenever
Yet another possibility is to use by lazy
:
class Foo(val rawLabel: String) {
var count = 0
val label: String by lazy { println("initializing via lazy!"); rawLabel.toUpperCase() }
fun something() {
count += 1
println("$label: something() was called $count times")
}
}
fun main() {
val foo = Foo("foo")
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, label
is initialized lazily. Whenever we first attempt to use label
, the lambda expression will be executed, and that value is what we see as the value of label
. In this case, the lambda expression takes a constructor parameter and converts it into all caps, so our output is:
initializing via lazy!
FOO: something() was called 1 times
FOO: something() was called 2 times
FOO: something() was called 3 times
the final count was 3
In truth, we would not need to use by lazy
here. Since the constructor parameter is available at initialization time, and it is not changing, we could just initialize label
normally:
class Foo(val rawLabel: String) {
var count = 0
val label = rawLabel.toUpperCase()
fun something() {
count += 1
println("$label: something() was called $count times")
}
}
val foo = Foo("foo")
foo.something()
foo.something()
foo.something()
As with lateinit var
, by lazy
is good for cases where you cannot initialize the val
up front, for whatever reason (e.g., onCreate()
has not yet been called on your Android Activity
). And, as with lateinit var
, you run the risk of crashing if the conditions are not ready for your lambda expression to be evaluated.
The difference amounts to “push” versus “pull”. With lateinit var
, we try to initialize the property as soon as it is practical, pushing a value into it. With by lazy
, we hope that we do not try referencing it before the lambda expression will work… but once it is referenced, we “pull” its initial value from other data, using the lambda expression to derive the value that we want to use.
by lazy
is one example of a broader pattern of property delegates, which we will explore much later in the book.
Wait a Minute! Was That… a Semicolon?
Our label
property not only had a lazy
property delegate, but the lazy
lambda expression had a rarity in Kotlin: a semicolon.
As with Java, a semicolon in Kotlin serves as a statement terminator. However, in Kotlin, a newline also can serve as a statement terminator, which is why we rarely see semicolons used in that role. In this case, the lazy
lambda expression contains two statements: one to log a message mentioning that the lambda expression was being evaluated, and one to convert the string to uppercase.
Fake Properties
Another way to initialize a property… is to have it only look to outsiders like it is a regular property.
We usually distinguish between accessing a property and calling a function by the existence of parentheses. anObject.someProperty
references a property, while anObject.thatFunction()
calls a function.
In reality, Kotlin is not nearly that simple. It is possible to call functions without using parentheses — forEach
, for example, is a function. Beyond that, there are ways to define properties such that they really are implemented by functions.
We will explore this way of setting up “fake properties” more much later in the book.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.