We have already learned about null safety in Kotlin. In this topic, we will consider how to deal with null in collections, which are more complex than other data types. We will also discuss what convenient methods are used for nullable elements.
Collections and nullable
Nullable collections and non-nullable collections with nullable elements inside them are two sides of the same coin. Besides, we have to realize the difference between empty collections and nullable collections. Let’s look at all four cases:
val list = listOf<String>()
var nullableList: List<Int>? = listOf<Int>(1, 2, 4, 6)
val listWithNullableElements: List<Int?> = listOf<Int?>(1, 2, 4, null, null)
var absolutelyNullableList: List<Int?>? = listOf<Int?>(1, 2, 4, null, null)In the first case, we have a simple empty list. We can deal with it like with a common list and don’t need to worry about NullPointerException. This list is real and non-null, it is only empty.
In the second case, we have a nullable list: the elements in such a list aren’t nullable, they must be real integer numbers. But the variable nullableList can be null. And when we work with a nullable list, we have to use the safe call operator, checks, the Elvis operator, etc. Like this:
val list: List<Int> = nullableList? ?: listOf<Int>()In the third case, we have a list with nullable elements. It has a non-nullable type, but the elements inside are nullable.
val num: Int = listWithNullableElements[1]? ?: 150In the fourth case, we combine the approaches from the second and third cases:
val num: Int = absolutelyNullableList?[1]? ?: 150The basic principle is: if we can return an empty collection — it’s better than returning null and using nullable types. However, sometimes we do need to deal with null collections. For instance, if we declare a variable (var, not val) that can receive something or receive null as a value (it’s equal to a “non-existing element”, “no answer”, or “no result”).
Creating non-null collections from sequences with null elements
Sometimes you have element sequences with null, and you need to use them to create a collection without null. In that case, you can use the special functions listOfNotNull() and setOfNotNull(), which help us to delete all null elements and return read-only collections with non-null type by default. Let’s consider how it works:
val list = listOfNotNull(1, null, 50, 404, 42, null, 42, 404) // [1, 50, 404, 42, 42, 404]
val set = setOfNotNull(1, null, 50, 404, 42, null, 42, 404) // [1, 50, 404, 42]All null elements are deleted from the new collection. If your element sequence has only null elements, these methods will return an empty (not null!) collection. Remember that if you need a mutable collection, just convert it with toMutableList() or toMutableSet().
Functions for nullable collections
Kotlin has some convenient tools for collections with nullable elements: isNullOrEmpty(), getOrNull(), firstOrNull(), lastOrNull(), and randomOrNull(). Let’s look at them!
The function isNullOrEmpty() returns true if the collection is empty or equal to null. In all other cases, it returns false.
val emptySet: Set<Int>? = setOf()
val nullSet: Set<Int>? = null
val set = setOf<Int?>(null, null)
println(emptySet.isNullOrEmpty()) // true because the collection is empty
println(nullSet.isNullOrEmpty()) // true because the collection is equal to null
println(set.isNullOrEmpty()) // false because the collection has two elements with null valueThe function getOrNull() returns one element of a list or an array, but if this element doesn't exist, it returns null (it doesn’t work with Set).
val list = listOf(0, 1, 2)
println(list.getOrNull(2)) // 2
println(list.getOrNull(3)) // null because this list doesn’t have a fourth element and numbering starts with 0You may say that it is possible to use just list[3], but in that case, we’ll receive an exception while getOrNull() does return a value in every case. The function randomOrNull() works like the previous one: it returns null if the collection is empty and a random element in all other cases.
val list = listOf(0, 1, 2)
val list1 = listOf<Int>()
println(list.randomOrNull()) // returns some element
println(list1.randomOrNull()) // null because the collection is emptyThe functions firstOrNull() and lastOrNull() allow us to set specific conditions. And if there is at least one element that satisfies the condition, they return it.
val list = listOf(0, 1, 1, 2, 5, 7, 6)
val num = list.firstOrNull { it > 3 }
val num1 = list.lastOrNull { it == 1 }Min and max elements with nullable
There are a lot of convenient comparison tools for Kotlin collections – and for nullable elements, too. Here they are:
minOrNull()/maxOrNull()– return the max or min collection element ornullif the collection is empty.minByOrNull()/maxByOrNull()– return the first max or min collection element that satisfies the condition ornull.minOfOrNull()/maxOfOrNull()– return the value of the element’s characteristic tagged in the condition ornull.minWithOrNull()/maxWithOrNull()– return the first element that satisfies the condition specified in thecompareBy {}block ornull.minOfWithOrNull()/maxOfWithOrNull()– return the value of the element’s characteristic tagged in the condition specified in thecompareBy {}block ornull.
We only mention these functions here – you can find the details and examples in the topic "Aggregate operations on collections".
However, there is a little tricky thing: all these functions have their own twins without “OrNull”. Once upon a time these twins were legitimate tools. But in Kotlin 1.4.0, these twins, like min(), max(), minBy(), maxBy(), minWith(), and maxWith() were renamed to minOrNull(), maxOrNull(), and so on, and the old guys were tagged as deprecated. In Kotlin 1.7.0, however, they were reintroduced as non-nullable alternatives to their own “OrNull” counterparts. Now they return a collection element or throw an exception. Be careful with them!
Conclusion
So, we've considered collections with nullable elements and some convenient methods to work with them. Here are the important points:
There are nullable collections, collections with nullable elements, and empty collections in Kotlin. All of them are different.
The functions
listOfNotNull()andsetOfNotNull()allow us to create non-null collections from nullable sequences of data.We can check if the collection is empty or contains elements that satisfy some condition and be sure that we will not receive an exception.
We may select and display collection elements or their parameters with compare functions
maxOrNull()/minOrNull(), etc.These functions have non-null twins that return some value or an exception instead of
nullin the case ofmaxOrNull(), etc.