Photo by freestocks.org on Unsplash
Generics give us the capability of defining classes and functions that perform the same operations over different data types without changing a single line of code. Collections, for instance, make heavy usage of type arguments to traverse the data uniformly, ignoring the specific data type for each item.
But generics can be cumbersome when variance comes into play. Every now and then, we find ourselves wondering why some function accepts a set of numbers gratefully, but refuses (and even complains!) when having to work with a set of integers.
In this entry, we will explore the foundations of generics in order to understand how these mechanisms work under the hood. But first, let’s quickly review a few related concepts.
Data types in Generics
Generic classes receive a data type as an input parameter. So, in fact, we can say that these classes have two different types:
- the base type, this is, the type of the Generic class.
Ex: List<>, Pair<>, Map<>…
- the type parameter (or type argument), meaning the data type for the element contained.
Ex: List<String>, Pair<Int, Int>, Map<String, Any>…
Classes and types
Class and type are usually used as synonims, but there are a few subtle differences to take into account.
Classes can be seen as abstractions (or templates) that set both structure and behaviour for instances. On the other hand, (data) types are just constraints that define the values that can be stored on a certain variable.
In Kotlin, due to its nullability system, each class generates 2 different types. Consider the class “String”, for instance:
var name : String = "John"
var nullableName : String? = null
Moreover, when talking about types, let’s not forget that:
- each type can be seen as as subtype of itself. So “String” derives from “String”, for instance.
- nullable types are defined as supertypes for each non-nullable type associated. This means that “String?” is the supertype of “String”, but not the other way around.

As we will see, when working with generics, relations between data types get a little more complicated. According to our definition, Set is a class (this is, a “template” for storing several objects and managing them) and Set<String>, Set<Number> or Set<Any> are just some of the possible types available. But what is the relation between them…?
Mutable and immutable objects
Kotlin promotes the usage of immutable data, as stated in the principles of functional programming. When it comes down to collections, it offers two different class hierachies:
- one for just accessing (reading) data from a collection.
Ex: Iterable, Collection, List, Set, etc.
- another one for modifying (writing) data into a collection.
Ex: MutableIterable, MutableCollection, MutableList…
Variance: relation between types
In general, the term variance basically refers to the relation between generic types that have the same base class but different type arguments. Variance gives us the answer to questions such as: “Are they in the same class hierarchy?” or “Is one of them a subtype of the other?”.
NOTE: subtype here refers to the fact that an instance of the derived type can be used wherever an instance of base type is expected, so they are exchangeable.
So variance allows us to answer questions like: “if Int is a subtype of Number, then List<Int>… is a subtype of List<Number>? Or is it a supertype? Or should I go back to web programming…?”
Once the relation between components is set through variance, then we can use them accordingly. For instance, we can send as parameter a subtype if the function called expects a base type or any of its derived elements.
“Variance sets the relation between generic elements that share the same base class”
Variance can be painful sometimes, so why should we care about it? The answer is simple: variance is crucial to avoid data type inconsistencies and prevent crashes during the program execution, so it’s definitely worthy.
Depending on the relation between generic types, we have 3 possible scenarios:
- Invariance: generic components have no relation at all.
- Covariance: generic component with a derived type parameter is considered a child of the component with a base type parameter.
- Contravariance: generic component with a derived type parameter is considered a parent of the other (this one is probably the less intuitive but we will try to shed some light here as well).
For each scenario, variance is explicitly set using modifiers (in, out…) preceding the type parameter. So these keywords can be used:
- in the type parameter defined when declaring a generic class
- in the type parameter specified when declaring a function that uses generics
//XXX: type parameter in class declaration (no modifier)...
class ParametricClass<S> {
//XXX: and in method declaration... (no modifier either)
fun <T> parametricMethod(data : List<T>) {...}
}
In the next sections, we will explore each one of these scenarios in detail.
Invariance: no relation at all between types
In Kotlin, a generic class is invariant by default on its type argument. This means that it has no relation with other elements that have the same base type but different type arguments.
Consider the following function declaration:
fun manageList(list : MutableList<Number>) {}
Can we call this function sending a “MutableList<Int>” instead? No, we can’t, it requires an exact match on the type argument, so code will only compile when sending a “MutableList<Number>”.
val numbers = mutableListOf<Number>(...)
val integers = mutableListOf<Int>(1, 2, 3)
manageList(numbers)//XXX: types match, ok!
manageList(integers)//XXX: mismatch, ko!
“Generics are invariant by default”
So, as we said before, although “Int” is a subtype of “Number”, invariance states that there is no relation between generic types “MutableList<Int>” and “MutableList<Number>”.

Covariance: preserving the subtype relation
As we’ve seen, although being quite restrictive, invariance is the default behaviour in order to mantain type safety. But sometimes we need a little bit of flexibility, so how do we “break” invariance, making our code more reusable?
Following with the previous example, suppose that in fact we require the previous function to work with both “MutableList<Number>” and “MutableList<Int>”. It all comes down to setting covariance on the type argument, adding the out modifier in the function declaration.
//XXX: accept lists of numbers or any derived type
fun manageList(list : MutableList<out Number>) {}
By setting covariance on the type argument, we are saying that the function is accepting collections of numbers and integers (or any other “Number” derived class).
In Java, covariance would be specified as:
//XXX: accept lists of some unknown type that is a subtype of T (or T itself)
void manageList(List<? extends T> list) {
...
}

With covariance, the subtype relation between individual types is respected when applied to generics. “Int” is a subtype of “Number”, and “MutableList<Int>” is a subtype of “MutableList<Number>”. Moreover, covariance allows generator functions (functions that return a value of the given type argument) to return instances of any type included in the hierarchy.
But when applying covariance in type arguments of classes, the out modifier can only be set if the related type is only produced (returned) by the class. This means that, inside that class, the type parameter can appear as the return of any function, but can’t be used as an input parameter. For this reason, classes with covariance on type parameters are usually called “Producers“.
class ItemProducer<out T> constructor(...) {
...
fun count() : Int
//XXX: ok because T appears in return position, so it can be marked as "out"
fun getItem(index : Int) : T
}
Contravariance: flipping the subtype relation
If the previous scenario was clear, then contravariance is a piece of cake! Either way, let’s check it too.
Variance is quite powerful, and it allow us to define classes or functions that accept as parameters an inverse hierarchy of components too, such as “MutableList<Int>” and “MutableList<Number>” (or any other of its supertypes).
In order to set contravariance, all we have to do is flip the subtype relation (“Int” instead of “Number”) and apply the in modifier preceding the type parameter:
//XXX: accept lists of ints or any super type
fun manageList(list : MutableList<in Int>) {}

In fact, subtyping is simply reversed. We can just think about it as a reflection for covariance. In this case, the relation between the type arguments has the opposite direction of the relation between individual types.
“Covariance maintains the subtype relation in Generics, but contravariance flips it”
In Java, contravariance would be specified as:
//XXX: accept lists of some unknown type whose supertype is T
void manageList(List<? super T> list) {
}
When applying contravariance in classes, the in modifier can only be set when the types are consumed (read) only. So the type arguments specified must be specified as parameters in the class functions. As you probably guessed, that’s why these classes are usually called “Consumers“.
class ItemComsumer<in T> constructor(...) {
...
fun count() : Int
//XXX: ok because T appears as function param
fun readItem(t : T) : Unit
}
Wrapping up
Keen in mind: by default, generics are invariant, so no relation for its type arguments is established. Covariance on parameter types respects the subtype relation, whereas contravariance just reverses it, as shown in the diagram:

As usual, check this link to explore the examples in more detail and have some fun with generics:
Write you next time!