Skip to main content

Kotlin Null Safety

Introduction

One of the most common pitfalls in many programming languages is the NullPointerException (often abbreviated as NPE), which occurs when you try to access a member of a null reference. This error has even been called "The Billion Dollar Mistake" by its creator, Tony Hoare.

Kotlin addresses this issue with its null safety feature, making null references explicit in the type system. This approach helps developers avoid NPEs and write more robust code. In this tutorial, we'll explore Kotlin's null safety mechanisms and how they can improve your code quality.

Nullable vs Non-Nullable Types

In Kotlin, by default, all types are non-nullable. This means that once you declare a variable of a certain type, it cannot hold a null value unless explicitly marked as nullable.

Non-Nullable Types

kotlin
var name: String = "John"
name = null // Compilation error: Null cannot be a value of a non-null type String

Nullable Types

To allow a variable to hold null values, you must explicitly mark it with a question mark (?):

kotlin
var name: String? = "John"
name = null // This is perfectly valid

Safe Calls Operator (?.)

When working with nullable types, you can use the safe call operator (?.) to safely access properties or methods:

kotlin
// Let's create a simple User class
class User(val name: String, val email: String?)

// Create a nullable user
val user: User? = User("John", "[email protected]")

// Without safe call - would cause NPE if user is null
// val userEmail = user.email

// With safe call - returns null if user is null
val userEmail: String? = user?.email

println(userEmail) // Output: [email protected]

// Chain of safe calls
val user2: User? = null
val user2Email = user2?.email
println(user2Email) // Output: null

Elvis Operator (?:)

The Elvis operator (?:) allows you to provide a default value when an expression evaluates to null:

kotlin
val user: User? = null
val userName = user?.name ?: "Anonymous"
println(userName) // Output: Anonymous

// You can also throw exceptions with Elvis
val email = user?.email ?: throw IllegalArgumentException("User email is required")

The !! Operator (Not-Null Assertion)

The not-null assertion operator (!!) converts any value to a non-null type and throws a NullPointerException if the value is null:

kotlin
var name: String? = "John"
val length = name!!.length // This works because name is not null
println(length) // Output: 4

name = null
// val length2 = name!!.length // This would throw NullPointerException
warning

The !! operator should be used with caution, as it defeats the purpose of null safety. Only use it when you are absolutely certain a value is not null or when you explicitly want to throw an exception if it is.

Smart Casts

Kotlin's smart cast feature automatically casts a nullable type to a non-nullable type after a null check:

kotlin
fun getStringLength(str: String?): Int {
// At this point, str is String?
if (str != null) {
// Inside this block, str is automatically cast to non-nullable String
return str.length
}
return 0
}

println(getStringLength("Hello")) // Output: 5
println(getStringLength(null)) // Output: 0

Safe Casts (as?)

The safe cast operator (as?) attempts to cast a value to the specified type, returning null if the cast is not possible:

kotlin
val obj: Any = "Hello"

// Regular cast
// val str: String = obj as String
// println(str) // Output: Hello

// Safe cast
val str1: String? = obj as? String
val int1: Int? = obj as? Int

println(str1) // Output: Hello
println(int1) // Output: null

Collections and Null Safety

Kotlin's null safety extends to collections as well:

kotlin
// List of nullable strings
val nullableList: List<String?> = listOf("A", null, "B", null, "C")

// Only process non-null elements
for (item in nullableList) {
item?.let {
println("Processing item: $it")
}
}
// Output:
// Processing item: A
// Processing item: B
// Processing item: C

// Filter out nulls
val nonNullList = nullableList.filterNotNull()
println(nonNullList) // Output: [A, B, C]

Practical Example: User Registration Form

Let's see a practical example of using null safety in a user registration scenario:

kotlin
data class RegistrationForm(
val username: String,
val email: String?,
val phoneNumber: String?,
val address: Address?
)

data class Address(
val street: String,
val city: String,
val zipCode: String
)

fun validateRegistration(form: RegistrationForm): String {
// Username is required
if (form.username.isBlank()) {
return "Username is required"
}

// Either email or phone must be provided
if (form.email.isNullOrBlank() && form.phoneNumber.isNullOrBlank()) {
return "Either email or phone number must be provided"
}

// If address is provided, all fields must be filled
form.address?.let {
if (it.street.isBlank() || it.city.isBlank() || it.zipCode.isBlank()) {
return "All address fields must be filled if address is provided"
}
}

// If everything is valid
return "Registration successful"
}

// Test cases
val validForm = RegistrationForm(
"johndoe",
"[email protected]",
null,
Address("123 Main St", "New York", "10001")
)

val invalidForm = RegistrationForm(
"janedoe",
null,
null,
null
)

println(validateRegistration(validForm)) // Output: Registration successful
println(validateRegistration(invalidForm)) // Output: Either email or phone number must be provided

Late Initialization with lateinit

Sometimes you need to declare a non-null property but initialize it later. The lateinit keyword can help with this:

kotlin
class MyViewModel {
private lateinit var dataRepository: DataRepository

fun initialize(repo: DataRepository) {
this.dataRepository = repo
}

fun loadData() {
// Using dataRepository safely after initialization
if (::dataRepository.isInitialized) {
dataRepository.fetchData()
} else {
throw IllegalStateException("DataRepository not initialized")
}
}
}

Nullable Types and Platform Interoperability

When working with Java code, Kotlin treats all Java types as "platform types," which can be either nullable or non-nullable:

kotlin
// Assuming getName() comes from a Java class and returns String
// In Kotlin, this return value is treated as a platform type (String!)
val name = javaObject.getName()

// You decide how to treat it:
val nonNullName: String = name // You take responsibility if it's null
val nullableName: String? = name // Safe approach

Summary

Kotlin's null safety features provide a powerful way to avoid null pointer exceptions and write more robust code:

  • Use non-nullable types by default
  • Mark variables that can be null with ?
  • Access nullable properties safely with the safe call operator (?.)
  • Provide default values with the Elvis operator (?:)
  • Use the not-null assertion (!!) only when absolutely necessary
  • Take advantage of smart casts after null checks
  • Use safe casts (as?) when type casting might fail
  • Utilize filterNotNull() for collections
  • Use lateinit when a property must be initialized before use but after construction

By embracing these null safety features, you'll write more reliable Kotlin code that's less prone to runtime exceptions.

Exercises

  1. Create a function that safely extracts the first character of a nullable string, returning null if the string is null or empty.
  2. Implement a function to safely get an element from a list at a given index, returning a default value if the index is out of bounds.
  3. Create a data class representing a product with some nullable properties, then write a function that validates the product data.
  4. Write a function that works with a potentially null external library object, handling the null case gracefully.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)