Kotlin — Mastering generics nullability

Kotlin — Mastering generics nullability

Image for post

Kotlin generics offer a few improvements over Java generic type system such as declaration use-site variance, start-projection syntax or reified type parameters. There is also separate aspect that I want to cover closely related to Kotlin type system ? nullability.

Before we move on it?s essential to understand difference between type parameter and type argument. Functions have parameters (variables declared inside function declaration) and arguments (actual value that is passed to a function). Similar terminology applies for generics. Blueprint or placeholder for a typed declared in generic class/interface/method is called type parameter while actual type used for generic type declaration passes do is called type argument.

When we define class using generic unbounded type parameter we can use both non-nullable and nullable types as type arguments. Sometimes we need to make sure that particular generic type is only parametrized with non-nullable type augments. It may be a class representing event, action performed by user, command or simply presenter that always needs a view. Let?s consider this example.

class Tree (val name:String)class Forest<T>//Forest class parameterized with non-nullable type argumentvar forestA: Forest<Tree>//Forest class parameterized with nullable type argumentvar forestB: Forest<Tree?>

We want to define generic type that will not accept nullable type arguments. The reason why we can use nullable type arguments is that each type parameter has implicit bound. It is Any? Class (the same class used as implicit super class when we create class without specifying parent class). Both below declarations are equivalent.

class Forest<T> //type parameter implicitly bounded by Any?class Forest<T : Any?> //type parameter explicitly bounded by Any?

We can use nullable type arguments because each non-nullable type is subtype of its nullable corresponding type (Any is a subtype of Any?, Activity is subtype of Activity?, Fragment is subtype of Fragment?). We can block ability to use nullable types as type arguments by explicitly defining non-nullable type parameter bound (T : Tree).

class Tree (val name:String)class Forest<T : Tree>var forestA: Forest<Tree>var forestB: Forest<Tree?> //error

Now we can?t pass nullable type argument (Tree?) to Forest class. Let?s update our Forest class to deal with another scenario. We will add constructor parameter accepting list of trees and last property to retrieve last tree.

class Forest<T : Tree>(private val list: List<T>) { val last: T get() = list.last()}

Notice that due to type inference we could skip type declaration of last property getter, but we will keep it, so it will be easier for us to understand upcoming examples. Let?s use our newly defined last property.

val treeList = listOf(Tree(“Redwood”), Tree(“Palm”))val forest = Forest<Tree>(treeList)//?val tree = forest.last //Inferred type Treeprintln(tree.name) //prints: Palm

We create instance of Forest class, call last property and print proper result. Everything seems to be working fine, but what will happen when we pass empty list to constructor?

val forest = Forest<Tree>(listOf())//?//error: NoSuchElementException: List is emptyval tree = forest.last println(tree.name)

Our application crashes because implementation of underlying last method has non-nullable return type and and simply throws NoSuchElementException when list has no items. Here is a simplified version of this method that is used by List class.

public fun <T>last(): T { if (isEmpty()) throw NoSuchElementException(?List is empty.?) return this[lastIndex]}

Instead of having nasty exception we want to handle this scenario internally inside our Forest class ? when list is empty we will return null otherwise we will return last Tree in the list. Let?s update Forest class type parameter bound to accept only nullable type arguments. We will also update last property implementation to match our new assumptions.

class Forest<T : Tree?>(private val list: List<T>) { val last: T get() = if(list.isEmpty()) null // error else list.last()}

Seems that we have everything in place, but this time the code will not compile. hmm? Why this is happening? The problem is that now Forest class can be parametrized with both nullable and non-nullable type arguments (non-nullable type is sub-type of nullable, so it?s valid candidate). Kotlin compiler treats this scenario as unsafe, because class behavior would not be consistent for nullable and non-nullable type arguments (there is always possibility that last will be null irrespective of type argument nullability).

To solve this problem we need to specify nullable return type using nullable type parameter by adding question mark to type parameter use-site (T?).

class Forest<T : Tree>(private val list: List<T>) { val last: T? get() = if(list.isEmpty()) null else list.last()}

T? means that last property will be always nullable regardless of potential type argument nullability. Notice that we restored type parameter T bound as non-nullable type Tree, because we want to store non-nullable types and deal with nullability only for certain scenarios (like non-existing last element). As Christophe B. suggested we can simplify this code like this:

class Forest<T : Tree>(private val list: List<T>) { val last: T? get() = list.lastOrNull()}

Let?s use our updated Forest class.

val forest= Forest<Tree>(listOf())//?val tree = forest.last //Inferred type Tree?println(tree?.name) //safe call operator is required

Let?s sum up the above solution.

  1. We parameterized Forest class with non-nullable type argument Tree.
  2. We specified non-nullable bound for type parameter to disallow parameterizing Forest class with nullable types as type arguments.
  3. We specified nullable type at type parameter use-site T?, because last property can return null if there are no elements in the list.

Notice that null check is required for accessing name property of retrieved tree because inferred type for tree variable is always nullable type (Tree?).

Some examples many seem simple, but my goal was to explain this the subject in the easiest possible way. By now you should have understanding how Kotlin type system nullability affects generic types creation and usage. You should also understand the difference between nullable type argument and nullable type parameter.

If you liked this article please click the ?, so other people can read it too. I would love to hear your thoughts in the comment section. Happy coding!

12

No Responses

Write a response