4

The Kotlin modifier that shouldn't be there

 2 years ago
source link: https://proandroiddev.com/the-modifier-that-shouldnt-be-there-77ff941f0529
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The Kotlin modifier that shouldn't be there

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:

1*fTOBwgKh4Yp1Bn9KxG6o1g.png?q=20
the-modifier-that-shouldnt-be-there-77ff941f0529

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

1*XgMOm-cWnTLdEPIo__WCcw.png?q=20
the-modifier-that-shouldnt-be-there-77ff941f0529

and indeed adding final would fix that:

1*33ITNiup6efFCcjf0ax-Cg.png?q=20
the-modifier-that-shouldnt-be-there-77ff941f0529

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.

1*Z10NQVem-wKaI_bO0MHfCw.png?q=20
the-modifier-that-shouldnt-be-there-77ff941f0529

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:

1*rpWGsmeOar-T5AOPf8D0Yw.png?q=20
the-modifier-that-shouldnt-be-there-77ff941f0529

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
classFindPeopleToFollowViewModel

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

for proofread and discussion

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK