Bianca Dragomir I am a React/React Native software developer and a tech writer, mainly focusing on frontend development topics.

Understanding Kotlin generics

9 min read 2639

Understanding Kotlin Generics

Kotlin is a programming language that was developed by JetBrains, the team behind IntelliJ IDEA, Pycharm, and other IDEs that make our lives as programmers easier. Kotlin does this by allowing us to write more concise code while being safer than other programming languages, such as Java.

Let’s take a look at Kotlin, why we need Kotlin generics, and dive into the concept of generics in depth.

Here’s what we’ll cover in this guide:

Why do developers need Kotlin?

The JetBrains team initially created Kotlin for internal use. Java was making the JetBrains codebase difficult to maintain, so they were in need of a more modern language.

Since Kotlin provides complete Java interoperability, it is easy to use on both projects being built from the ground up and existing codebases where the developers prefer to adopt the new approach. Kotlin has replaced Java as the preferred language for developing Android apps.

Currently, more than 80 percent of the top 1,000 apps from the Google Play Store use Kotlin, and backend developers are also starting to use it more and more. In addition, Kotlin Multiplatform is becoming increasingly popular, while Jetpack Compose is widely used on new projects.

We must note that Kotlin is a statically typed programming language, meaning we have to specify and be aware of the types of all variables at compile time.

Dynamically typed languages, such as Python, can offer the developer more flexibility when writing code. However, this practice is prone to runtime errors since variables can take any value of any type.

By specifying types for our variables, we can stay consistent and write more robust code that is also easier to maintain and debug. Why? Because compile-time errors are easier to spot and fix than runtime errors.

Why do we need generics in Kotlin?

Using a strongly typed language such as Kotlin might make a developer feel constrained sometimes.

We made a custom demo for .
No really. Click here to check it out.

We all liked Python when we were first-year computer science students because it lets us write anything. But because we didn’t know how to write proper code and other best practices, we ended up with a bowl of impossible-to-debug spaghetti code.

Don’t worry, there’s a wonderful solution to this exact problem! This solution is referred to as generic programming and is usually bundled with definitions that are stuffy and difficult to decipher.

In this article, we are going to use a laid-back approach focused on helping you get the concepts, reviewing:

  • What are generics?
  • What is the purpose of generics?
  • The difference between class, subclass, type, and subtype
  • Defining variance, covariance, invariance, and contravariance
  • How the Kotlin generics in and out keywords map to these terms

Towards the end of this reading, you’ll be fully prepared to use Kotlin generics in any project.

What are generics?

Generic programming is a way of writing our code in a flexible manner like we would in a dynamically-typed language. At the same time, generics allow us to write code safely and with as few compile-time errors as possible.

Using generics in Kotlin enables the developer to focus on creating reusable solutions, or templates, for a wider range of problems.

We can define a template as a partially filled solution that can be used for a variety of situations. We fill in the gaps when we actually use that solution (for instance, a class) and provide an actual type for it.

The difference between class, subclass, type, and subtype

When reading about generic types and inheritance, we’ll notice that the words class, subclass, type, and subtype are thrown around. What exactly is the difference between them?

A class is a blueprint of the objects that will be instantiated using it. These objects will inherit all the fields and methods that were declared in that class.

A subclass is a class that is derived from another class. Simply put, our subclass will inherit all the methods and fields that exist in the parent class.

We can then say these objects all have the same type defined by the class. Types should mainly focus on the interface of an object, not on the concrete implementation that can be found in the classes that are used when instantiating objects.

A subtype will be created when a class inherits a type from another class or implements a specific interface.

Now let’s return to generics and understand why we need them in a statically typed language like Kotlin.

Example of how generics can be useful

In the next code snippet, we define a stack that can be used for the sole purpose of handling integers:

class IntStack {

    private val elements: MutableList<Int> = ArrayList() 

    fun pop(): Int {
        return elements.removeLast()
    }

    fun push(value: Int) {
        elements.add(value)
    }
    // ...
}

Nothing fancy for now. But what happens if we need to store integer strings, or even puppies? Then we’d need to create two more classes: StringStack and PuppyStack.

Would the puppy stack do anything differently than the integer stack (except for being more adorable, obviously)? Of course not. As a result, there’s no need to create separate classes for each case. It’s enough to create a generic stack that can be used anywhere in our project:

class Stack<T> {

    private val elements: MutableList<T> = ArrayList()

    fun pop(): T {
        return elements.removeLast()
    }

    fun push(value: T) {
        elements.add(value)
    }
    // ...
}

Now we can use this data structure for stacking anything we want, no matter how adorable or dull it is.

But what if we need to impose some restrictions on the situations where our generic class can be used? These restrictions might implement behaviors that don’t apply to every single situation. This is where we introduce the concepts of variance, covariance, contravariance, and invariance.

Variance

Variance refers to the way in which components of different types relate to each other. For example, List<Mammal> and List<Cat> have the same base type (List), but different component types (Mammal and Cat).

It’s important to understand how lists of these two types would behave in our code and whether or not they are compatible with our purpose. For instance, take a look at the following code snippet:

open class Mammal { ... }
class Cat: Mammal() { ... }
class Dog: Mammal() { ... }

val animals: MutableList<out Mammal> = mutableListOf()
animals.add(Dog(), Cat())

In the code above, variance tells us that a Dog and a Cat will have the same rights in a list that’s defined as List<Mammal>.

The code below would work, too:

val dogs: List<Dog> = listOf(Dog())
val mammal: Mammal = dog.first()

Covariance

Covariance allows you to set an upper boundary for the types that can be used with the class. If we were to illustrate this concept using the stack that we defined above, we’d use the keyword out.

For a concrete example, we can take a look at the definition and an instantiation of List<> from Kotlin:

public interface List<out E> : Collection<E> { ... }
...
val numbers: List<Number> = listOf(1, 2, 3.0, 4, ...)

By doing something like this, we are essentially defining an upper bound for the elements of this list and relaxing the limitations put on our generic types.

In other words, whenever we retrieve an element from the list created above, we know for sure that the element will be of at least type Number. As a result, we can safely rely on any attribute or behavior of the Number class when working with the elements of our list.

Let’s take a look at a different example:

class PetOwner<T>

// !!! This won't work: it's a type mismatch
val petOwner1: PetOwner<Animal> = PetOwner<Cat>()        

// This will work: we tell the compiler that petOwner2 accepts lists of its type's subtypes too
val petOwner2: PetOwner<out Animal> = PetOwner<Cat>()

Covariance is very useful when we want to limit our usage to subtypes only:

val mammals: List<out Mammal > = listOf(Dog(), Cat())
mammals.forEach { mammal -> mammal.move() }

By instantiating our mammals list with the above syntax, we ensure that only subtypes of the type Mammal can be contained in and retrieved from a list.

In a more real-world scenario, we could think of a superclass User and two subclasses Moderator and ChatMember. These two subclasses can be stored together in a list defined as List<out User>.

Contravariance

But what if we had a case where we wanted to do an operation only on those members that have a certain degree of rights and responsibilities in our scenario?

This is where we’d want to set a lower boundary. More specifically, when using the syntax Stack<in T>, we are able to only manipulate objects that are at most of type T.

val superUsersList: MutableList<in Moderator> = mutableListOf()

With the above syntax, we are therefore creating a list that will only accept objects of type Moderator and above (such as User, the supertype of User — if it has one — and so on).

Here’s a more interesting example of contravariance in Kotlin:

val userComparator: Comparator<User> = object: Comparator<User> {
  override fun compare(firstUser: User, secondUser: User): Int {
    return firstUser.rank - secondUser.rank
  }
}
val moderatorComparator: Comparator<in Moderator> = userComparator

The above syntax is correct. What we’re doing is defining a comparator that can be used for any kind of user. Then we declare a comparator that only applies to moderators and assigns to it the users comparator. This is acceptable since a Moderator is a subtype of User.

How is this situation contravariant? The userCompare comparator specializes in a superclass, whereas the moderator comparator is a subclass that can be assigned a value that depends on its superclass.

The equivalent of these concepts in Java is as follows:

  • List<out T> in Kotlin is List<? extends T> in Java
  • List<in T> in Kotlin is List<? super T> in Java

Invariance

Invariance is easy to understand: basically, every class that you define with a generic type with no in or out keyword will be considered to be invariant. This is because there will be no relationship between the types that you created using generics.

Let’s look at an example to clear things up:

open class Animal

class Dog: Animal()

val animals: MutableList<Animal> = mutableListOf()
val dogs: MutableList<Dog> = mutableListOf()

In the above example, we see that there’s a clear relationship between Dog and Animal: the former is a subtype of the latter. However, we can’t say the same about the types of the two list variables. There is no relationship between those two. Therefore, we can say that List is invariant on its type parameter.

All Kotlin generic types are invariant by default. For example, lists are invariant — as we saw above. The purpose of the in and out keywords is to introduce variance to a language whose generic types don’t allow it otherwise.

Restricting the usage of generics

When using generics in Kotlin, we must also avoid misusing our methods and classes in ways that can lead us to errors. We must use in and out to impose declaration-site variance for our types.

In some situations, we must use generics with our method definitions such that the parameters passed to them will respect a set of prerequisites. These prerequisites ensure that our code can actually run. Let’s check out an example:

open class User

class Moderator: User()

class ChatMember: User()

Let’s say that we wanted to sort our users based on a criterion (their age, for example). Our User class has an age field. But how can we create a sorting function for them? It is easy, but our users must implement the Comparable interface.

More specifically, our User class will extend the Comparable interface, and it will implement the compareTo method. In this way, we ensure that a User object knows how to be compared to another user.

fun <T: Comparable<T>> sort(list: List<T>): List<T> {
    return list.sorted()
}

From the above function declaration, we understand that we can strictly use the sort method on lists that contain object instantiations of classes that implement the Comparable interface.

If we were to call the sort method on a subtype of Animal, the compiler would throw an error. However, it will work with the User class since it implements the compareTo method.

Type erasure in Kotlin

It is also interesting to note that Kotlin, just like Java, performs type erasure when compiling our code. This means that it first checks our types and either confirms that we used them correctly or throws errors that tell us to do better next time. Afterward, it strips the type information from our generic types.

The compiler wants to make sure that types are not available to us at runtime. This is the reason why the following code would not compile:

class SimpleClass {

    fun doSomething(list: List<String>): Int {
...
    }

    fun doSomething(list: List<Int>): Int {
    ...
    }
}

fun main() {
    val obj = SimpleClass()
}

This is because the code compiles correctly, with the two methods having actually different method signatures. However, type erasure at compile time strips away the String and Int types that we used for declaring our lists.

At runtime, we only know that we have two lists, without knowing what type the objects are from those two lists. This outcome is clear from the error that we get:

Exception in thread "main" java.lang.ClassFormatError: Duplicate method name "doSomething" with signature "(Ljava.util.List;)I" in class file SimpleClass

When writing our code, it’s worth keeping in mind that type erasure will happen at compile time. If you would really want to do something like we did in the above code, you’d need to use the @JvmName annotation on our methods:

@JvmName("doSomethingString") 
fun doSomething(list: List<String>): Int {
...
}

@JvmName("doSomethingInt")  
fun doSomething(list: List<Int>): Int {
...
}

Conclusion

There are several things that we covered in this article in order to understand Kotlin generics.

We first clarified the difference between a type and a class when working in Kotlin (and any object-oriented language). Afterward, we introduced the concept of generics and their purpose.

To dive deeper into Kotlin generics, we checked out some definitions accompanied by examples that showed us how generics are used and implemented in Kotlin compared to Java, a very similar language.

We also understood variance, covariance, contravariance, and invariance in Kotlin and learned how (and when) to apply these concepts in our projects by the means of the in and out keywords.

The key takeaway of this article is that generics can be used in our code in order to keep it simple, maintainable, robust, and scalable. We ensure that our solutions are as generic as possible when they need to be — it’s also important not to complicate our lives by trying to make everything generic.

Sometimes this practice could make everything more difficult to follow and to put into practice, so it’s not worth using generics if they don’t bring true value to us.

By using generics in Kotlin, we avoid using casts, and we catch errors at compile time instead of runtime. The compiler ensures that we use our types correctly before performing type erasure.

I hope that this helped you and that it clarified the concepts related to Kotlin generics. Thanks a lot for reading!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Bianca Dragomir I am a React/React Native software developer and a tech writer, mainly focusing on frontend development topics.

Leave a Reply