Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Elvis Operator for Nullable Expressions

Recommended resources

Hello! Let's dive into the next piece of Kotlin's null-safety puzzle.

In our last lesson, we explored the safe call operator (?.), which allows you to access properties or methods on a nullable variable. You learned that an expression like user.address?.street evaluates to the street name if the address exists, but evaluates to null if the address is missing. This is great for preventing NullPointerExceptions, but it leaves you with a nullable result.

Often, you don't want null; you want a sensible default value instead. This lesson introduces the perfect tool for that job: the Elvis operator (?:). For a Java developer accustomed to ternary operators or if-else blocks for providing defaults, the Elvis operator is a refreshingly concise and powerful alternative.

Learning Outcome: Use the Elvis operator (?:) to provide a default value for a nullable expression.


1. From Safe Calls to Default Values

Let's start with the scenario from our last lesson. We have a nullable variable and we use a safe call on it:

class Address(val street: String)
class User(val name: String, val address: Address?)

val userWithNoAddress = User("Bob", null)

// This expression results in a nullable String (String?)
val street = userWithNoAddress.address?.street 

If we want to print the street name, but print "Unknown" if it's null, we could use a standard if-else check. This pattern will be familiar to you from Java.

val streetWithDefault: String
if (street != null) {
    streetWithDefault = street
} else {
    streetWithDefault = "Unknown"
}
println(streetWithDefault) // Output: Unknown

This works, but it's verbose. Kotlin provides a much more idiomatic way to handle this exact situation.


2. The Elvis Operator (?:)

The Elvis operator, written as ?:, is used to provide a default value when an expression on its left-hand side is null.

The syntax is: [nullable expression] ?: [default value expression]

  • If the [nullable expression] is not null, its value is returned.
  • If the [nullable expression] is null, the [default value expression] on the right is evaluated and its result is returned.

Let's rewrite our previous example using the Elvis operator:

val userWithNoAddress = User("Bob", null)

// If userWithNoAddress.address?.street is null, use "Unknown" instead.
val streetWithDefault = userWithNoAddress.address?.street ?: "Unknown"

println(streetWithDefault) // Output: Unknown

Notice two key things:

  1. The code is reduced to a single, highly readable line.
  2. The type of streetWithDefault is String, not String?. The Elvis operator effectively "unwraps" the nullable type by guaranteeing a non-null value is always available.

The image below provides a great visual breakdown of this logic.

Caption: This diagram illustrates the flow of the Elvis operator (`?:`). It checks the value on the left; if it's not null, that value is used. If it is null, the default value on the right is used instead.

To see this in action and hear a brief explanation, watch the first minute of this video.

How to Use the Kotlin Elvis Operator ?:

This video from Donn Felker clearly demonstrates the problem of handling nulls and how the Elvis operator provides a clean solution.

Watch from the beginning to 01:27. The video first shows the traditional if statement approach and then introduces the Elvis operator as a more concise alternative.


3. Comparing with Traditional Null Checks

The primary benefit of the Elvis operator is its conciseness and readability compared to traditional if-else blocks. For someone with your Java background, seeing the direct comparison can be illuminating.

Kotlin Elvis vs. Traditional Null Checks

The article 'Kotlin Elvis Operator: The Key to Safer, Null-Proof Code' provides a clear side-by-side comparison that highlights the advantages of the Elvis operator.

Read the section titled 'Kotlin Elvis vs. Traditional Null Checks'. It effectively contrasts the verbosity of an if-else block with the elegance of the one-line Elvis operator expression.


4. More Than Just Default Values: return and throw

The expression on the right side of the Elvis operator doesn't have to be a simple value. It can be any expression, including return or throw. This unlocks powerful patterns for control flow, such as creating guard clauses.

Using Elvis with return

You can use the Elvis operator to exit a function early if a required value is null.

fun printShippingLabel(user: User) {
    // If the user has no address, exit the function.
    val address = user.address ?: return

    // Because of the check above, the compiler now smart-casts 'address'
    // to a non-nullable 'Address' for the rest of the function.
    println("Shipping to:")
    println(address.street) 
}

printShippingLabel(User("Alice", Address("123 Main St")))
// Output:
// Shipping to:
// 123 Main St

printShippingLabel(User("Bob", null)) // No output, function returns early

Using Elvis with throw

Similarly, you can use it to enforce invariants in your code by throwing an exception if a value that should not be null is, in fact, null. This is a common pattern for validating parameters or state in backend services.

fun getRequiredConfigValue(key: String): String {
    val configMap = mapOf("host" to "127.0.0.1") // Port is missing
    
    // Retrieve the value, or throw an exception if it's not found.
    return configMap[key] ?: throw IllegalArgumentException("Missing config for key: $key")
}

println(getRequiredConfigValue("host")) // Output: 127.0.0.1
// The following line will throw an exception:
// println(getRequiredConfigValue("port")) 

This video from the official Kotlin channel provides an excellent walkthrough of these advanced use cases.

Return and Throw with the Elvis Operator | Kotlin Tips

This 'Kotlin Tips' video demonstrates how to use the Elvis operator with return and throw for more robust control flow.

Watch the segments on using the Elvis operator with return (00:56 - 03:22) and throw (03:32 - 04:31). Pay attention to how the compiler understands that the variable is non-null after the Elvis operator check.


5. Code Exercise

Let's put this into practice. Open your IntelliJ IDEA project and create a new Kotlin file.

  1. Setup:
    Define the same User and Address classes we've been using.

    class Address(val street: String, val city: String)
    class User(val name: String, val address: Address?)
    
  2. Task 1: Provide a Default Value
    In your main function, create a User instance with a null address. Write a line of code that retrieves the user's city using the safe call and Elvis operators. If the address is null, the city should default to "N/A". Print the result.

    fun main() {
        val userWithoutAddress = User("Charlie", null)
        
        // Your code here: get the city or default to "N/A"
        // val city = ...
        
        // println("City: $city")
    }
    
  3. Task 2: Throw an Exception
    Write a function named mustHaveStreet that accepts a User object. The function should return the street from the user's address. If the address or the street is null for any reason, it should throw an IllegalStateException with a descriptive message. Test this function with both a user that has an address and one that does not.

    fun mustHaveStreet(user: User): String {
        // Your code here: return the street or throw an exception
    }
    

Take a few minutes to complete these tasks before moving on.



Conclusion

You've now added another essential Kotlin null-safety tool to your arsenal. The Elvis operator (?:) is the idiomatic way to substitute a default value when dealing with nullable types, leading to safer and more readable code.

Key Takeaways:

  • The Elvis operator (?:) evaluates the expression on its left; if it's null, it returns the value of the expression on its right.
  • It allows you to convert a nullable type (e.g., String?) to a non-nullable type (String) by providing a non-null default.
  • It is far more concise than a traditional if/else block for providing default values.
  • The right-hand side of the operator can be an expression like return or throw, enabling powerful control-flow patterns like guard clauses.

Next Lesson Preview:

We've learned how to handle nulls safely with ?. and ?:. But what about situations where you, the developer, are absolutely certain that a nullable variable will not be null at a specific point in your code? For these cases, Kotlin provides the not-null assertion operator (!!). In the next lesson, we will explore how to use it and, more importantly, discuss the risks involved and when it should be avoided.