96

How (not) to get NullPointerExceptions in Kotlin – AndroidPub

 6 years ago
source link: https://android.jlelse.eu/how-not-to-get-nullpointerexceptions-in-kotlin-d0d2f80e10a8?gi=3571778fc251
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.

How (not) to get NullPointerExceptions in Kotlin

When I first heard about Kotlin, it didn’t take me long to realize how wonderful the language was. The first presentation I witnessed made it sound quite interesting, but the minute I decided to try it out, I truly fell in love with it. One of the many things that attracted me most was how you could avoid the so dreaded NullPointerException by properly handling optional types. Far from the perplexity I felt when I first discovered that feature in Swift five years ago, it now felt much more natural in Kotlin.

1*Fga_MBIqVmjI8KeWCAVh0A.png

Anyway, using Kotlin didn’t make the NullPointerException disappear at once. The thing is that nullable types are no guaranty you won’t have NullPointerExceptions anymore. They’re just a very powerful tool to prevent them… if you understand the logic behind them. However, after years of Java, manipulating variables that could be null at any point as if it was a natural thing, people don’t necessarily understand the paradigm shift.

Getting NullPointerExceptions is actually as simple as using a double bang. But even forbidding its use won’t be enough. As Christina Lee put it in her Google I/O talk, you need to think about what null means for you now. Not all variables can be null, so why should one be nullable and what does it imply?

Null doesn’t have to be your default value

The trick we fell for as java coders is that null was such an easy default value for our variables. Literally. If you didn’t provide a value for a class member, it was initialized with null. If you started to implement an abstract method in your class, your IDE would stub its body with a return null. But it didn’t have to. In lots of cases, non-null values can be totally valid default values. Think about primitive types: until autoboxing arrived in Java 5, most of us never bothered to wrap numbers in Objects. As such, they couldn’t be null, and yet, we had ways to handle those cases. Because when you’re dealing with numbers, zero is very often a totally reasonable default value.

You can even expand that idea to other types: an empty String is often better than a null one, and an empty list will generally do a better job than a null one (see Effective Java, by Joshua Bloch : “Item 43: Return empty arrays or collections, not nulls). You could even choose to use some sealed class to differentiate between a populated object and a placeholder or empty one (see Refactoring, by Martin Fowler: Introduce Null object). The takeaway here is that null is not necessarily your only choice as a default value, and you should probably think twice before resorting to it. You could even guard certain variables against ever risking to receive a null value (in case some rogue Java code tried to sneak it behind your back) with a simple Elvis operator:

val nonNullableString = nullableString ?: ""

Who needs a default value anyway?

If you can’t find any other suitable value for your objects until they’re properly initialized, then your second question should be whether you actually need that temporary value. Based on your business logic, it might be obvious that whenever you will need to access a certain variable, that value will not be null. Yet that doesn’t mean that this value exists at all times. In this sort of cases, you don’t actually need a default value. You just need a way to avoid dealing with your variable when it’s not ready. If you never read a certain value when it is null, you don’t risk the NullPointerException and will never need to declare it as nullable.

If this seems confusing, let me give you an example: imagine an Android activity in which you decide to save a certain view, but don’t manipulate it until the activity has actually started. You can be certain that everywhere you try to access that variable, the view exists and your app won’t crash.

This doesn’t mean that the view attribute will always have a value though. Until you call setContentView(), calling your variable would return null. But since you don’t need it then, you have a few options to save you the trouble of dealing with that undesired state. The first that might come to mind is not necessarily the safest. The lateinit keyword allows you to explicitly state that a variable is never null when it is read, even if it does not immediately contain a value.

lateinit var myView: View

This implies a certain rigor on your part to make sure no scenario exists where that condition could be violated. Be a bit negligent with it and you will soon realize that NullPointerExceptions still happen in Kotlin!

Other choices include the use of custom getters or delegates. In the previous example, you don’t have to actually store the view reference (unless you notice some performance issues and feel the need to restrict findViewById calls to limit your CPU consumption). So you could simply replace that field with a dynamic calculation:

val myView: View
get() = findViewById(R.id.my_view)

Or use a delegate to compute that value. Which could even store the value if you‘re low on CPU resources. That’s what we generally call lazy loading, and Kotlin even has a ready-made delegate for you to use:

val myView: View by lazy { findViewById(R.id.my_view) }

Ain’t afraid of no null

Finally, if those questions don’t give you the expected answers and you still have to handle the null case, now comes the time to roll your sleeves up and dive in the hazardous world of optional types. But that’s no excuse for dropping double bangs on your precious variables. Especially when you realize that Kotlin developers got you covered with a clever smartcast to simplify your syntax. It will only work on safe cases though, namely local variables or immutable ones (a mutable class member could be altered by another thread between the moment you checked its nullity and the moment you actually use it).

So what do we do when it fails? How do you safely unwrap your risky variable without compromising the clarity of your program? The safe call operator can be of some use, but it should be handled with care: if you start using it everywhere, you will end up with some very unpredictable code where some lines get executed and others don’t for no obvious reason (well, the obvious reason is that some variables were null, but which ones? good luck tracking down the lines that were definitely executed and the ones that fell through). It’s the “try with empty catch” blocks all over again…

So basically, what you need is to clearly mark portions of code associated with a nullability condition. The way to do it is up to you. It could be with a local copy of your variable that you can check (and smartcast), a bit like this :

val copy = someNullableAttribute
if (copy != null) { ... }

It could be with the combination of a safe-call operator and one of the idiomatic let, run, apply, also functions:

someNullableValue?.let { 
useSafely(it)
}

You could even choose an Elvis operator for that. Don’t forget that throw or return statements have the Nothing type, which means you could virtually affect them to any variable in your code, as in:

val nonNullValue = nullableEntry ?: throw(SomeException())
val nonNullValue = nullableEntry ?: return

Another option is to use the require function to check your arguments’ values and throw an IllegalArgumentException if needed:

require(nullableEntry != null) {
"Entry shouldn't be null"
}

What matters most is not which option you choose, but rather how clearly you isolated the portions of your code that will be run or skipped in case of a null value. Code clarity is paramount in that case.

Welcome to a non-null world

Nullability is not such a complicated science. It’s just too easy to get wrong. Give it the attention it needs and you will soon see tangible results and improvements in your code. The more practice you get with Kotlin, the more you’ll come to love it. But don’t fool yourself into thinking that the language itself will solve all of your problems, as if by magic. Kotlin is but a tool, and a powerful one that is, but how you use that tool can make a world of difference. So next time you declare a variable in your code, don’t rush to add that little question mark and ask yourself the important questions.

Don’t stop at nullability though: lots of other praised concepts in Kotlin imply that you get used to a certain discipline and take on some healthier habits (immutability, open or closed inheritance, lack of checked exceptions…). That’s what will truly make your code less error prone in the end, and it could even improve your Java code too!

“I’m not a great programmer; I’m just a good programmer with great habits.” Kent Beck


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK