Kotlin
In Kotlin what is the idiomatic way to deal with nullable values referencing or converting them
Kotlin, a modern, pragmatic programming language, has garnered immense popularity for its conciseness, interoperability with Java, and perhaps most notably, its robust approach to null safety. Unlike many languages where a NullPointerException (NPE) can be a frequent and frustrating runtime error, Kotlin tackles this issue head-on by making nullability an explicit part of its type system. This design philosophy significantly reduces the likelihood of these dreaded crashes, leading to more stable and reliable applications. Understanding Kotlin’s null safety features and mastering the idiomatic way to deal with nullable values, referencing or converting them, is crucial for any developer aiming to write high-quality, maintainable Kotlin code. This guide delves deep into the tools and techniques Kotlin provides, helping you navigate the world of potential nulls with confidence and clarity.
Understanding Nullable vs. Non-Nullable Types in Kotlin
At the core of Kotlin’s null safety is the distinction between nullable and non-nullable types. By default, all types in Kotlin are non-nullable, meaning a variable of type String cannot hold a null value. If you attempt to assign null to a non-nullable type, the compiler will immediately flag it as an error, preventing potential NPEs at compile time rather than runtime. This fundamental design choice shifts the burden of null checks from the developer at runtime to the compiler during development, enhancing code reliability.
To explicitly allow a variable to hold a null value, you append a question mark (?) to its type declaration, creating a nullable type. For example, String? indicates a variable that can either hold a String or null. This explicit declaration forces developers to acknowledge and handle the possibility of a null value whenever they interact with a nullable type, making null-related issues transparent and manageable. According to JetBrains, the creators of Kotlin, this strict compile-time null safety has been a key factor in improving developer productivity and reducing bugs in large-scale projects.
This deliberate design choice encourages developers to think about nullability from the outset. Instead of defensively checking for nulls everywhere, you’re prompted to handle them only where they are genuinely possible. This approach not only prevents crashes but also leads to cleaner, more predictable code that clearly communicates its intent regarding null values. It’s a proactive measure that prevents issues before they even arise, which is far more efficient than debugging elusive runtime errors.
Essential Operators for Handling Nullable Values
Kotlin provides a suite of operators specifically designed to work with nullable types, allowing you to safely reference or convert values without explicit null checks that can clutter your code. These operators are the backbone of idiomatic Kotlin null safety.
The Safe Call Operator (?.)
The safe call operator, ?., is perhaps the most fundamental tool for dealing with nullable values. It allows you to call a method or access a property on an object only if that object is not null. If the object is null, the entire expression evaluates to null, and no operation is performed. This elegantly prevents NPEs when attempting to access members of a potentially null reference.
val name: String? = "Alice" val length = name?.length // If name is "Alice", length is 5. If name is null, length is null. val address: String? = null val streetLength = address?.length // streetLength will be null
This operator is invaluable for chaining calls, making code concise and readable. You can chain multiple safe calls, and if any part of the chain evaluates to null, the entire chain short-circuits to null, avoiding any intermediate NPEs.
The Elvis Operator (?:)
Often, when you have a nullable expression, you want to provide a default value if the expression evaluates to null. This is where the Elvis operator, ?:, shines. It evaluates the expression on its left side; if it’s not null, its value is used. If it is null, the value on the right side of the operator is used instead.
val userDisplayName: String? = null val displayName = userDisplayName ?: "Guest" // displayName will be "Guest" val city: String? = "New York" val displayCity = city ?: "Unknown" // displayCity will be "New York"
The Elvis operator is particularly useful for providing fallback values, ensuring that your program always has a valid value to work with, even when dealing with potentially null inputs. It’s a common pattern for safely converting nullable types to non-nullable types by providing a default, non-null value.
The Not-Null Assertion Operator (!!)
While Kotlin strongly encourages safe null handling, there are rare cases where you might be absolutely certain that a nullable value will not be null at a specific point in your code. For such scenarios, you can use the not-null assertion operator, !!. This operator converts any nullable type to its non-nullable counterpart. However, if the value turns out to be null at runtime, it will throw an NPE.
val email: String? = "test@example.com" val domain = email!!.substringAfter("@") // Safe, as email is not null val phone: String? = null // val digits = phone!!.length // This would throw a NullPointerException at runtime
The !! operator should be used sparingly and only when you have external guarantees (e.g., from a Java library or a specific API contract) that the value will not be null. Overuse of !! undermines Kotlin’s null safety features and reintroduces the very problem Kotlin aims to solve. It’s often considered a last resort or a tool for interop with less null-safe codebases.
Leveraging Scope Functions for Null Handling
Kotlin’s standard library includes several [val something: Xyz? = createPossiblyNullXyz() something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?" But if I check null first it is allowed, why?
val something: Xyz? = createPossiblyNullXyz() if (something != null) { something.foo() }
How do I change or treat a value as not null without requiring the if check, assuming I know for sure it is truly never null? For example, here I am retrieving a value from a map that I can guarantee exists and the result of get() is not null. But I have an error:
val map = mapOf("a" to 65,"b" to 66,"c" to 67) val something = map.get("a") something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"
The method get() thinks it is possible that the item is missing and returns type Int?. Therefore, what is the best way to force the type of the value to be not nullable?
Note: this question is intentionally written and answered by the author (Self-Answered Questions), so that the idiomatic answers to commonly asked Kotlin topics are present in SO. Also to clarify some really old answers written for alphas of Kotlin that are not accurate for current-day Kotlin.
First, you should read all about Null Safety in Kotlin which covers the cases thoroughly.
In Kotlin, you cannot access a nullable value without being sure it is not null (Checking for null in conditions), or asserting that it is surely not null using the !! sure operator, accessing it with a ?. Safe Call, or lastly giving something that is possibly null a default value using the ?: Elvis Operator.
For your 1st case in your question you have options depending on the intent of the code you would use one of these, and all are idiomatic but have different results:
val something: Xyz? = createPossiblyNullXyz() // access it as non-null asserting that with a sure call val result1 = something!!.foo() // access it only if it is not null using safe operator, // returning null otherwise val result2 = something?.foo() // access it only if it is not null using safe operator, // otherwise a default value using the elvis operator val result3 = something?.foo() ?: differentValue // null check it with `if` expression and then use the value, // similar to result3 but for more complex cases harder to do in one expression val result4 = if (something != null) { something.foo() } else { ... differentValue } // null check it with `if` statement doing a different action if (something != null) { something.foo() } else { someOtherAction() }
For the “Why does it work when null checked” read the background information below on smart casts.
For your 2nd case in your question in the question with Map, if you as a developer are sure of the result never being null, use !! sure operator as an assertion:
val map = mapOf("a" to 65,"b" to 66,"c" to 67) val something = map.get("a")!! something.toLong() // now valid
or in another case, when the map COULD return a null but you can provide a default value, then Map itself has a getOrElse method:
val map = mapOf("a" to 65,"b" to 66,"c" to 67) val something = map.getOrElse("z") { 0 } // provide default value in lambda something.toLong() // now valid
-–
Background Information:
Note: in the examples below I am using explicit types to make the behavior clear. With type inference, normally the types can be omitted for local variables and private members.
More about the !! sure operator
-——————————–
The !! operator asserts that the value is not null or throws an NPE. This should be used in cases where the developer is guaranteeing that the value will never be null. Think of it as an assert followed by a smart cast.
val possibleXyz: Xyz? = ... // assert it is not null, but if it is throw an exception: val surelyXyz: Xyz = possibleXyz!! // same thing but access members after the assertion is made: possibleXyz!!.foo()
read more: !! Sure Operator
-–
More about null Checking and Smart Casts
-—————————————–
If you protect access to a nullable type with a null check, the compiler will smart cast the value within the body of the statement to be non-nullable. There are some complicated flows where this cannot happen, but for common cases works fine.
val possibleXyz: Xyz? = ... if (possibleXyz != null) { // allowed to reference members: possiblyXyz.foo() // or also assign as non-nullable type: val surelyXyz: Xyz = possibleXyz }
Or if you do a is check for a non-nullable type:
if (possibleXyz is Xyz) { // allowed to reference members: possiblyXyz.foo() }
And the same for ‘when’ expressions that also safe cast:
when (possibleXyz) { null -> doSomething() else -> possibleXyz.foo() } // or when (possibleXyz) { is Xyz -> possibleXyz.foo() is Alpha -> possibleXyz.dominate() is Fish -> possibleXyz.swim() }
Some things do not allow the null check to smart cast for the later use of the variable. The example above uses a local variable that in no way could have mutated in the flow of the application, whether val or var this variable had no opportunity to mutate into a null. But, in other cases where the compiler cannot guarantee the flow analysis, this would be an error:
var nullableInt: Int? = ... public fun foo() { if (nullableInt != null) { // Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time" val nonNullableInt: Int = nullableInt } }
The lifecycle of the variable nullableInt is not completely visible and may be assigned from other threads, the null check cannot be smart cast into a non-nullable value. See the “Safe Calls” topic below for a workaround.
Another case that cannot be trusted by a smart cast to not mutate is a val property on an object that has a custom getter. In this case, the compiler has no visibility into what mutates the value and therefore you will get an error message:
class MyThing { val possibleXyz: Xyz? get() { ... } } // now when referencing this class... val thing = MyThing() if (thing.possibleXyz != null) { // error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter" thing.possiblyXyz.foo() }
read more: Checking for null in conditions
-–
More about the ?. Safe Call operator
-————————————-
The safe call operator returns null if the value to the left is null, otherwise continues to evaluate the expression to the right.
val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable() // "answer" will be null if any step of the chain is null val answer = possibleXyz?.foo()?.goo()?.boo()
Another example where you want to iterate a list but only if not null and not empty, again the safe call operator comes in handy:
val things: List? = makeMeAListOrDont() things?.forEach { // this loops only if not null (due to safe call) nor empty (0 items loop 0 times): }
In one of the examples above we had a case where we did an if check but have the chance another thread mutated the value and therefore no smart cast. We can change this sample to use the safe call operator along with the let function to solve this:
var possibleXyz: Xyz? = 1 public fun foo() { possibleXyz?.let { value -> // only called if not null, and the value is captured by the lambda val surelyXyz: Xyz = value } }
read more: Safe Calls
-–
More about the ?: Elvis Operator
-———————————
The Elvis operator allows you to provide an alternative value when an expression to the left of the operator is null:
val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()
It has some creative uses as well, for example throw an exception when something is null:
val currentUser = session.user ?: throw Http401Error("Unauthorized")
or to return early from a function:
fun foo(key: String): Int { val startingCode: String = codes.findKey(key) ?: return 0 // ... return endingValue }
read more: Elvis Operator
-–
Null Operators with Related Functions -————————————
Kotlin stdlib has a series of functions that work really nicely with the operators mentioned above. For example:
// use ?.let() to change a not null value, and ?: to provide a default val something = possibleNull?.let { it.transform() } ?: defaultSomething // use ?.apply() to operate further on a value that is not null possibleNull?.apply { func1() func2() } // use .takeIf or .takeUnless to turn a value null if it meets a predicate val something = name.takeIf { it.isNotBlank() } ?: defaultName val something = name.takeUnless { it.isBlank() } ?: defaultName
-–
Related Topics -————-
In Kotlin, most applications try to avoid null values, but it isn’t always possible. And sometimes null makes perfect sense. Some guidelines to think about:
- in some cases, it warrants different return types that include the status of the method call and the result if successful. Libraries like Result give you a success or failure result type that can also branch your code. And the Promises library for Kotlin called Kovenant does the same in the form of promises.
- for collections as return types always return an empty collection instead of a null, unless you need a third state of “not present”. Kotlin has helper functions such as emptyList() or emptySet() to create these empty values.
- when using methods which return a nullable value for which you have a default or alternative, use the Elvis operator to provide a default value. In the case of a Map use the getOrElse() which allows a default value to be generated instead of Map method get() which returns a nullable value. Same for getOrPut()
- when overriding methods from Java where Kotlin isn’t sure about the nullability of the Java code, you can always drop the ? nullability from your override if you are sure what the signature and functionality should be. Therefore your overridden method is more null safe. Same for implementing Java interfaces in Kotlin, change the nullability to be what you know is valid.
- look at functions that can help already, such as for String?.isNullOrEmpty() and String?.isNullOrBlank() which can operate on a nullable value safely and do what you expect. In fact, you can add your own extensions to fill in any gaps in the standard library.
- assertion functions like checkNotNull() and requireNotNull() in the standard library.
- helper functions like filterNotNull() which remove nulls from collections, or listOfNotNull() for returning a zero or single item list from a possibly null value.
- there is a Safe (nullable) cast operator as well that allows a cast to non-nullable type return null if not possible. But I do not have a valid use case for this that isn’t solved by the other methods mentioned above.](https://kotlinlang.org/docs/scope-functions.html)