Null safety
In the beginner tour, you learned how to handle null values in your code. This chapter covers common use cases for null safety features and how to make the most of them.
Smart casts and safe casts
Kotlin can sometimes infer the type without explicit declaration. When you tell Kotlin to treat a variable or object as if it belongs to a specific type, this process is called casting. When a type is automatically cast, like when it's inferred, it's called smart casting.
is and !is operators
Before we explore how casting works, let's see how you can check if an object has a certain type. For this, you can use the is and !is operators with when or if conditional expressions:
ischecks if the object has the type and returns a boolean value.!ischecks if the object doesn't have the type and returns a boolean value.
For example:
as and as? operators
To explicitly cast an object to any other type, use the as operator. This includes casting from a nullable type to its non-nullable counterpart. If the cast isn't possible, the program crashes at runtime. This is why it's called the unsafe cast operator.
To explicitly cast an object to a non-nullable type, but return null instead of throwing an error on failure, use the as? operator. Since the as? operator doesn't trigger an error on failure, it is called the safe operator.
You can combine the as? operator with the Elvis operator ?: to reduce several lines of code down to one. For example, the following calculateTotalStringLength() function calculates the total length of all strings provided in a mixed list:
The example:
Uses the
totalLengthvariable as a counter.Uses a
forloop to loop through every item in the list.Uses an
ifand theisoperator to check if the current item is a string:If it is, the string's length is added to the counter.
If it is not, the counter isn't incremented.
Returns the final value of the
totalLengthvariable.
This code can be reduced to:
The example uses the .sumOf() extension function and provides a lambda expression that:
For each item in the list, performs a safe cast to
Stringusingas?.Uses a safe call
?.to access thelengthproperty if the call doesn't return anullvalue.Uses the Elvis operator
?:to return0if the safe call returns anullvalue.
Null values and collections
In Kotlin, working with collections often involves handling null values and filtering out unnecessary elements. Kotlin has useful functions that you can use to write clean, efficient, and null-safe code when working with lists, sets, maps, and other types of collections.
To filter null values from a list, use the filterNotNull() function:
If you want to perform filtering of null values directly when creating a list, use the listOfNotNull() function:
In both of these examples, if all items are null values, an empty list is returned.
Kotlin also provides functions that you can use to find values in collections. If a value isn't found, they return null values instead of triggering an error:
maxOrNull()finds the highest value. If one doesn't exist, returns anullvalue.minOrNull()finds the lowest value. If one doesn't exist, returns anullvalue.
For example:
This example uses the Elvis operator ?: to return a printed statement if the functions return a null value.
You can use the singleOrNull() function with a lambda expression to find a single item that matches a condition. If one doesn't exist or there are multiple items that match, the function returns a null value:
Some functions use a lambda expression to transform a collection and return null values if they can't fulfill their purpose.
To transform a collection with a lambda expression and return the first value that isn't null, use the firstNotNullOfOrNull() function. If no such value exists, the function returns a null value:
To use a lambda expression to process each collection item sequentially and create an accumulated value (or return a null value if the collection is empty) use the reduceOrNull() function:
This example also uses the Elvis operator ?: to return a printed statement if the function returns a null value.
Explore Kotlin's standard library to find more functions that you can use to make your code safer.
Early returns and the Elvis operator
In the beginner tour, you learned how to use early returns to stop your function from being processed further than a certain point. You can use the Elvis operator ?: with an early return to check preconditions in a function. This approach is a great way to keep your code concise because you don't need to use nested checks. The reduced complexity of your code also makes it easier to maintain. For example:
In this example:
There is a
Userdata class that has properties for the user'sid,nameand list of friends.The
getNumberOfFriends()function:Accepts a map of
Userinstances and a user ID as an integer.Accesses the value of the map of
Userinstances with the provided user ID.Uses an Elvis operator to return the function early with the value of
-1if the map value is anullvalue.Assigns the value found from the map to the
uservariable.Returns the number of friends in the user's friends list by using the
sizeproperty.
The
main()function:Creates three
Userinstances.Creates a map of these
Userinstances and assigns them to theusersvariable.Calls the
getNumberOfFriends()function on theusersvariable with values1and2that returns two friends for"Alice"and one friend for"Bob".Calls the
getNumberOfFriends()function on theusersvariable with value4, which triggers an early return with a value of-1.
You may notice that the code could be more concise without an early return. However, this approach needs multiple safe calls because the users[userId] might return a null value, making the code slightly harder to read:
Although this example checks only one condition with the Elvis operator, you can add multiple checks to cover any critical error paths. Early returns with the Elvis operator prevent your program from doing unnecessary work and make your code safer by stopping as soon as a null value or invalid case is detected.
For more information about how you can use return in your code, see Returns and jumps.
Practice
- Validate user data with safe casts and the Elvis operator
You are developing a notification system for an app where users can enable or disable different types of notifications. Complete the
getNotificationPreferences()function so that:The
validUservariable uses theas?operator to check ifuseris an instance of theUserclass. If it isn't, return an empty list.The
userNamevariable uses the Elvis?:operator to ensure that the user's name defaults to"Guest"if it isnull.The final return statement uses the
.takeIf()function to include email and SMS notification preferences only if they are enabled.The
main()function runs successfully and prints the expected output.
data class User(val name: String?) fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> { val validUser = // Write your code here val userName = // Write your code here return listOfNotNull( /* Write your code here */) } fun main() { val user1 = User("Alice") val user2 = User(null) val invalidUser = "NotAUser" println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false)) // [Email Notifications enabled for Alice] println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true)) // [SMS Notifications enabled for Guest] println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true)) // [] }data class User(val name: String?) fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> { val validUser = user as? User ?: return emptyList() val userName = validUser.name ?: "Guest" return listOfNotNull( "Email Notifications enabled for $userName".takeIf { emailEnabled }, "SMS Notifications enabled for $userName".takeIf { smsEnabled } ) } fun main() { val user1 = User("Alice") val user2 = User(null) val invalidUser = "NotAUser" println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false)) // [Email Notifications enabled for Alice] println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true)) // [SMS Notifications enabled for Guest] println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true)) // [] }- Find a single active subscription with singleOrNull()
You are working on a subscription-based streaming service where users can have multiple subscriptions, but only one can be active at a time. Complete the
getActiveSubscription()function so that it uses thesingleOrNull()function with a predicate to return anullvalue if there is more than one active subscription:data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? // Write your code here fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // null }data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? { return subscriptions.singleOrNull { subscription -> subscription.isActive } } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // null }data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? = subscriptions.singleOrNull { it.isActive } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // null }- Filter active usernames with mapNotNull()
You are working on a social media platform where users have usernames and account statuses. You want to see the list of currently active usernames. Complete the
getActiveUsernames()function so that themapNotNull()function has a predicate that returns the username if it is active or anullvalue if it isn't:data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List<User>): List<String> { return users.mapNotNull { /* Write your code here */ } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // [alice123, charlie99] }data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List<User>): List<String> { return users.mapNotNull { user -> if (user.isActive) user.username else null } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // [alice123, charlie99] }data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List<User>): List<String> = users.mapNotNull { user -> user.username.takeIf { user.isActive } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // [alice123, charlie99] }- Validate stock with early returns and the Elvis operator
You are working on an inventory management system for an e-commerce platform. Before processing a sale, you need to check if the requested quantity of a product is valid based on the available stock.
Complete the
validateStock()function so that it uses early returns and the Elvis operator (where applicable) to check if:The
requestedvariable isnull.The
availablevariable isnull.The
requestedvariable is a negative value.The amount in the
requestedvariable is higher than in theavailablevariable.
In all of the above cases, the function must return early with the value of
-1.fun validateStock(requested: Int?, available: Int?): Int { // Write your code here } fun main() { println(validateStock(5,10)) // 5 println(validateStock(null,10)) // -1 println(validateStock(-2,10)) // -1 }fun validateStock(requested: Int?, available: Int?): Int { val validRequested = requested ?: return -1 val validAvailable = available ?: return -1 if (validRequested < 0) return -1 if (validRequested > validAvailable) return -1 return validRequested } fun main() { println(validateStock(5,10)) // 5 println(validateStock(null,10)) // -1 println(validateStock(-2,10)) // -1 }