Table of Contents
- Understanding Nullability in Kotlin
- Declaring Nullable Types
- Safe Calls (?.)
- The Elvis Operator (?:)
- Non-Null Assertion Operator (!!)
- Safe Casts (as?)
- Smart Casts
- Late-Initialized Properties
- The
letFunction with Nullable Types - Nullability in Collections
- Platform Types and Java Interoperability
- Best Practices for Null Safety
- Conclusion
- References
1. Understanding Nullability in Kotlin
In Kotlin, all variables are non-null by default. This means you cannot assign null to a variable unless you explicitly declare it as nullable. This strict distinction is enforced at compile time, preventing many null-related errors before your code runs.
Non-Nullable Types
A non-nullable type (e.g., String, Int, User) guarantees that the variable will never hold a null value. Attempting to assign null to a non-nullable variable results in a compile error:
var name: String = "Alice" // Non-nullable
name = null // Compile error: Null can not be a value of a non-null type String
Why This Matters
By making non-null the default, Kotlin ensures that you only deal with nulls when you intend to. This reduces the cognitive load of tracking which variables might be null and makes your code more predictable.
2. Declaring Nullable Types
To allow a variable to hold null, declare it as a nullable type by appending a ? to the type name (e.g., String?, Int?, User?).
Syntax
var nullableName: String? = "Bob" // Nullable
nullableName = null // Valid: Can assign null
Key Behavior
Nullable types restrict operations that could cause an NPE. For example, you cannot directly call methods or access properties on a nullable variable without additional checks:
val length = nullableName.length // Compile error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Kotlin’s compiler blocks this to prevent accidental NPEs. To work with nullable types safely, use the features covered in the following sections.
3. Safe Calls (?.)
The safe call operator (?.) allows you to call methods, access properties, or invoke functions on a nullable variable without risking an NPE. If the variable is null, the entire expression evaluates to null instead of throwing an error.
Basic Usage
val nullableString: String? = "Hello"
val length = nullableString?.length // length = 5 (non-nullable Int?)
val nullString: String? = null
val nullLength = nullString?.length // nullLength = null (no error)
Nested Safe Calls
Safe calls can be chained to access nested properties or methods. If any element in the chain is null, the entire expression returns null:
data class Address(val city: String?)
data class Person(val address: Address?)
val person: Person? = Person(Address("Paris"))
val city = person?.address?.city // city = "Paris" (all non-null)
val nullPerson: Person? = null
val nullCity = nullPerson?.address?.city // nullCity = null (no error)
Use Case
Safe calls are ideal for cases where you need to access nested data (e.g., JSON parsing, object graphs) and want to gracefully handle missing values.
4. The Elvis Operator (?:)
The Elvis operator (?:) provides a default value when a nullable expression evaluates to null. It acts as a shorthand for “if the left side is null, use the right side; otherwise, use the left side.”
Syntax
val result = nullableExpression ?: defaultValue
Examples
val name: String? = null
val displayName = name ?: "Guest" // displayName = "Guest" (default used)
val age: Int? = 25
val userAge = age ?: 0 // userAge = 25 (left side is non-null)
Key Notes
- The right-hand side (
defaultValue) is only evaluated if the left-hand side isnull(short-circuit evaluation). - The result of
a ?: bis non-null ifbis non-null. In the example above,displayNameisString(non-nullable), notString?.
5. Non-Null Assertion Operator (!!)
The non-null assertion operator (!!) forcefully converts a nullable type to a non-nullable type. If the value is null, it throws an NullPointerException (NPE).
Syntax
val nonNullValue = nullableValue!!
Example
val nullableText: String? = "Hello"
val length = nullableText!!.length // length = 5 (safe here)
val nullText: String? = null
val riskyLength = nullText!!.length // Throws NPE: KotlinNullPointerException
Warning
Use !! only when you are 100% certain the value is not null. Overusing it undermines Kotlin’s null safety guarantees and reintroduces NPE risks. Prefer safe calls or the Elvis operator instead.
6. Safe Casts (as?)
The safe cast operator (as?) attempts to cast a value to a target type. If the cast fails, it returns null instead of throwing a ClassCastException.
Syntax
val result = value as? TargetType // Result is TargetType? (nullable)
Example
val obj: Any = "Not a number"
val number = obj as? Int // number = null (cast fails, returns null)
val strObj: Any = "123"
val strNumber = strObj as? String // strNumber = "123" (cast succeeds)
Combining with Elvis Operator
Use as? with ?: to provide a fallback for failed casts:
val value: Any = 42
val stringValue = value as? String ?: "Unknown" // stringValue = "Unknown" (cast to String fails)
7. Smart Casts
Kotlin’s smart casts automatically convert a nullable type to a non-nullable type after a null check. The compiler tracks your checks and infers the variable’s nullability within the checked scope.
With if Statements
fun printLength(text: String?) {
if (text != null) {
// text is smart cast to non-nullable String
println("Length: ${text.length}")
} else {
println("text is null")
}
}
With when Expressions
fun processValue(x: Any?) {
when (x) {
is String -> println("String length: ${x.length}") // x is smart cast to String
is Int -> println("Int value: $x") // x is smart cast to Int
null -> println("x is null")
else -> println("Unknown type")
}
}
Limitations
Smart casts work best with val (immutable) variables. For var (mutable) variables, the compiler cannot guarantee the value hasn’t changed between the check and usage, so smart casts may not apply:
var mutableText: String? = "Hello"
if (mutableText != null) {
mutableText = null // Value changed after check
println(mutableText.length) // Compile error: mutableText is still nullable
}
8. Late-Initialized Properties
Sometimes you can’t initialize a property immediately (e.g., dependency injection, Android’s onCreate). Use lateinit to declare a non-nullable property that will be initialized later.
Syntax
lateinit var property: Type
Example
class UserProfile {
lateinit var userName: String // Non-nullable, initialized later
fun loadData() {
userName = fetchUserNameFromNetwork() // Initialize here
}
fun displayName() {
println(userName) // Safe to use after loadData()
}
}
Key Rules
lateinitonly works withvar(mutable) properties.- The property must be initialized before use; accessing an uninitialized
lateinitproperty throwsUninitializedPropertyAccessException. - Check initialization status with
::property.isInitialized(Kotlin 1.2+):
if (::userName.isInitialized) {
println(userName)
}
9. The let Function with Nullable Types
The scope function let is often used to execute code only if a nullable variable is non-null. It converts the nullable variable to a non-nullable parameter (it) inside the lambda.
Syntax
nullableVariable?.let { nonNullParam ->
// Code to run if nonNullParam is not null
}
Example
val nullableName: String? = "Alice"
nullableName?.let { name ->
println("Name length: ${name.length}") // Runs: name is non-null
}
val nullName: String? = null
nullName?.let {
println("This won't run") // Skipped: nullName is null
}
Use Cases
- Perform multiple operations on a non-null value without repeating null checks.
- Pass non-null values to functions that require non-null parameters:
fun logMessage(message: String) { /* ... */ }
val message: String? = "Hello"
message?.let { logMessage(it) } // it is non-null, so logMessage is called safely
10. Nullability in Collections
Collections can contain nullable elements (e.g., List<String?>). Use filterNotNull() to convert them to collections of non-nullable elements.
Example
val nullableList: List<String?> = listOf("Apple", null, "Banana", null, "Cherry")
val nonNullList: List<String> = nullableList.filterNotNull() // [Apple, Banana, Cherry]
Nullable Collection Types
List<String?>: A list that may containnullelements.List<String>?: A nullable list (the list itself may benull).List<String?>?: A nullable list that may containnullelements.
Handle nested nullability with safe calls and filterNotNull():
val nullableList: List<String?>? = listOf("A", null, "B")
val safeList = nullableList?.filterNotNull() ?: emptyList() // [A, B] (non-null list of non-null elements)
11. Platform Types and Java Interoperability
Java has no built-in null safety, so when calling Java code from Kotlin, Kotlin cannot know if a value is nullable. These are called platform types (denoted as Type! in Kotlin’s compiler messages).
Behavior of Platform Types
- Platform types are treated as “nullable but unchecked.” You can call methods on them directly, but this may throw NPEs if the value is
null. - Example: Java’s
String getString()could returnnull, so in Kotlin, it’s treated asString!(platform type).
Safe Handling
Treat platform types as nullable and use safe calls or !! (judiciously):
// Java method: public String getJavaString() { return null; }
val javaString: String! = getJavaString() // Platform type
// Safe: Use safe call
val length = javaString?.length // length = null (no NPE)
// Risky: Use !! (may throw NPE)
val riskyLength = javaString!!.length // Throws NPE if javaString is null
Explicit Nullability Annotations
To improve safety, annotate Java code with nullability annotations (e.g., @Nullable, @NonNull from JetBrains or Android):
// Java with annotations
import org.jetbrains.annotations.Nullable;
public class JavaUtils {
@Nullable
public static String getNullableString() { return null; }
}
Kotlin recognizes these annotations and treats getNullableString() as String? (nullable) instead of a platform type.
12. Best Practices for Null Safety
- Prefer Non-Null Types: Default to non-nullable types unless you explicitly need nullability.
- Use Safe Calls (?.) and Elvis (?:): Avoid
!!; use these operators to handle nulls gracefully. - Leverage
letfor Conditional Code: Usenullable?.let { ... }to run code only when a value is non-null. - Document
!!Usage: If you must use!!, add a comment explaining why the value is non-null. - Handle Platform Types Carefully: Treat Java interop types as nullable and validate inputs.
- Use
filterNotNull()for Collections: Clean up lists of nullable elements to avoid downstream null checks.
13. Conclusion
Kotlin’s null safety features transform how you handle nulls, turning runtime errors into compile-time checks and making your code more robust. By leveraging nullable types, safe calls, the Elvis operator, and other tools, you can eliminate most NPEs and write clearer, more maintainable code.
Remember: null safety is not just a feature—it’s a mindset. Embrace Kotlin’s defaults, use its tools wisely, and you’ll spend less time debugging nulls and more time building great software.