The Kotlin modifier that shouldn't be there

Danny Preussler
ProAndroidDev
Published in
4 min readMay 31, 2021

--

Photo by Belinda Fewings on Unsplash

Most Kotlin developers would agree that a val property is equivalent to a final property in Java. What if I tell you that this is not completely true and sometimes you might need a final val?

Opposite to Java, Kotlin properties are final by default unless they are explicitly marked as open! This would mean there is no need for the final keyword, right? Let’s Google that:

As the internet confirmed our hypothesis, I was quite surprised when Android Studio told me to addfinal to a val:

and indeed adding final would fix that:

So there is a final keyword for properties but why and when should we make a val final?

Let’s look at this behavior using a simple example:

class FinalBlog {

val someProperty: String = "some"

init {
print(someProperty + "thing")
}
}

“Everything works as expected here and the code will print “something” when the class is instantiated.

Let’s modify everything to be open:

open class FinalBlog {

open val someProperty: String = "some"

init {
print(someProperty + "thing")
}
}

This will trigger the same type of warning I got before.

This is very obvious when you think about it. The class can be subclassed and our property might be overridden. This could lead to unexpected side effects (which we will look into at the end of this post.).
We can fix this simply by removing theopen modifier from the field. So, althought the warning has the same cause, it’s not the exact same scenario my Android Studio was warning me on, there is no way I could add final here: non open already means final!

Let’s try something else:

interface BlogTopic {
val someProperty: String
}

open class FinalBlog: BlogTopic {

override val someProperty: String = "some"

init {
print(someProperty + "thing")
}
}

If the property is inherited from an interface, then it’s open by default!
Again, we will get the warning, we were looking for:

And this time adding the final modifier will fix it:

open class FinalBlog: BlogTopic {

final override val someProperty: String = "some"

init {
print(someProperty + "thing")
}
}

Voila, we got a final val 🥳

You will probably ask now: but in the original code, it was not an overridden val. Indeed, and also I didn't declare my class open!

The mystery has a simple solution: When I checked my class, I saw it:

@OpenForTesting
class FindPeopleToFollowViewModel

This is a commonly used marker that will trigger the Kotlin compiler plugin to open our class for testing so that they can be mocked there. But this opens up your class and makes every single field open, independent of test scope or not. And this is how I did see a val that actually will be open although I did not write it like this.

Hope you enjoyed this little excurse and if you also use that compiler plugin: You might want to consider mock maker inline from Mockito instead. Or, even better, try to use fewer mocks then you won't need this in the first place (TDD helps with that). 🤓

Aftermath

If we look at the example from above:

open class FinalBlog: BlogTopic {

override val someProperty: String = "some"

init {
print(someProperty + "thing")
}
}

Now let’s extend that class:

class ChildBlog: FinalBlog() {
override val someProperty = "another "
}

What do you think it will print once we initialize this class?

something” or “another thing”?

Actually, it prints “nullthing”! 🤯

This is what the warning was trying to tell us! To understand this issue, let’s jump into the byte code. (I use the “show Kotlin bytecode” and then choose the “decompile” to read it in Java format)

public class FinalBlog implements BlogTopic {
@NotNull
private final String someProperty = "some";
@NotNull
public String getSomeProperty() {
return this.someProperty;
}

Our Kotlin property is a getter with a field to hold the content.

Same for the child class:

public final class ChildBlog extends FinalBlog {
@NotNull
private final String someProperty = "another ";

@NotNull
public String getSomeProperty() {
return this.someProperty;
}

}

The problem is that we access the value from the parent constructor. As the getter is overridden, the version from the child class will be called. But the backing field is not initialized yet as we are still in the super constructor call. Therefore the value is null. (for a non-null declared field!).

But if we would have implemented the class without the backing field:

class ChildBlog: FinalBlog() {
override val someProperty
get
() = "another "
}

It would work. So the behavior is really unpredictable! Do not access non-final fields from the constructor.

Please keep an eye on warnings, they are there to help you!

PS: Thanks the all the amazing reviewers mentioned below plus mvndy for proofread and discussion

--

--

Android @ Soundcloud, Google Developer Expert, Goth, Geek, writing about the daily crazy things in developer life with #Android and #Kotlin