Kotlin - Generics & Type variance
Programming is all about abstractions.
Instead of instructing machines with binaries (1 and 0s), we created higher level abstractions. These higher level languages (abstractions) make it easy for us to instruct the machines.
During execution, machines should allocate and deallocate the memory. We added types
in abstraction to control how to allocate and de-allocate memory.
With Types
, it is easy to read, understand, and debug. Types allow compilers to allocate the right memory and ensure there are no runtime surprises.
Then we created statically typed languages. In a statically typed language, the compilers force you to have concrete types (either referenced or primitive) in (almost) every expression.
Note: Primitive types are types like Int, Float, Character, etc., and Referenced types are those that are created by you.
✨ Types are awesome right ✨
But what if I need to implement List
for both Integer
and String
?
You can create two separate implementation for both Integer
and String
. But it creates redundant code that is harder to maintain and debug.
Generics
will help you here.
Generics
Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. - Wikipedia
Generics add one more level of abstraction. With Generics, we can write a piece of code that is shared across various types.
Generics prevents us from writing repetitive code for every single type
that we need to implement. We provide some generic type information to the compiler. The compiler (or) runtime will do the rest.
In some lanaguages like C++, the compiler will expand
the Generic code with the necessary type information. This code generation happens during compilation.
For example if you have defined a class List
with a Generic type T
and used List for String
and Integer
. Then the compiler will generate a separate class for both String
and Integer
.
But why
T
?T
is generic way to define the type. You can very well use any letter there. Oh! hey,T
forType
.
✅ The generated code will have higher performance(⚡️), because the runtime knows exactly what to expect and how to deal with the types.
✅ There is no need for the runtime to unbox the value when consuming it and box it while returning it.
💥 Since there will be separate class for each type, this method will generate bloated code.
On the other hand in JVM languages, the compiler erases the type information completely.
The mechanism that Java uses is called
Type Erasure
.
Read more about Type Erasure here
The compilers will generate a class with no type information in it. Then JVM in runtime uses cast to get and set the value.
Thus the same class is used for both String
and Integer
.
✅ This leads to lesser code bloat and backward compatibility.
💥 On the other hand it leads to heap pollution and lesser performance.
In order to handle this, JVM provides cast iron guarantee
when defining a generics. Thus generics in Java are invariant
by default.
Suppose Class Animal
is a superType of Class Dog
.
open class Animal
class Dog: Animal()
Note: in order to extend the Animal class we need to
open
the animal class. In Kotlin, by default classes are final.
interface List<T> {
fun getAll(): List<T>
}
Then we have a generic List
interface defined. The interface has one method getAll
.
Now let us create AnimalList
and DogList
classes that implements the generic interface defined.
class AnimalList: List<Animal> {
override fun getAll(): List<Animal> {
TODO("not implemented")
}
}
class DogList: List<Dog> {
override fun getAll(): List<Dog> {
TODO("not implemented")
}
}
Covariance with out
Eventhough Animal
is the parent class of Dog
, List<Animal>
is not a parent class of List<Dog>
.
fun main() {
val dogList = DogList()
val d: List<Animal> = dogList.getAll() // 💥 Type mismatch
}
The compiler will not allow us to use subType
in place of superType
. That is the types Animal
and Dog
are not covariant
.
In Kotlin, we can inform the compiler to accept subType
in place of superType
with an out
keyword.
interface List<out T> {
fun getAll(): List<T>
}
The out
keyword tells the compiler that Animal
and Dog
are covariant types and it is okay to use them interchangably.
Now this will work,
fun main() {
val dogList = DogList()
val d: List<Animal> = dogList.getAll() // ✅
}
Use
out
for immutable types to avoid nasty runtime errors.
Contravariance with in
Contravariance
is the opposite of Covariance
. Contravariance
allows us to use superType
in place of subType
.
Consider the following class Inventory
. The Inventory class accepts an item
of type T
.
class Inventory<T> (item: T)
We have Shop
that holds the Inventory
.
class Shop<T> (items: Inventory<T>)
Consider that we have two shops, Animal and Dog Shop. Then for some reason, the animal shop decides to sell all their dogs to dog shop.
But the dogs in the AnimalShop are still tagged inside Inventory<Animal>
.
If we want Shop<Dog>
to accept all the Animal
from the Shop<Animal>
, then
fun main() {
val animal = Animal()
val dog = Dog()
val animalList = Inventory<Animal>(a)
val dogShop = Shop<Dog>(animalList) // 💥 Type error
}
But we are trying to use superType
in place of subType
, the compiler throws a type error.
We can instruct the compiler to accept the superType
in the place of subType
with an in
keyword in the type definition.
class Inventory<in T> (item: T)
This will tell the compiler to use the superType
in the place of subType
(i.e., Animal
in the place of Dog
).
A nice quote that can help remember these rules is: be liberal in what you accept and conservative in what you produce.
Types of Type variance
The type
that we pass in can either be a subType
, superType
.
There are following types of type variance in general:
- Covariant - This allows using
superType
in place ofsubType
. - Contravariant - This allows using
subType
in place ofsuperType
. - Bivariant - covariant and contravariant.
- Invariant - neither covariant nor contravariant.
Check out this post on how to generate a Full Stack application with Kotlin, React and Spring Boot using KHipster here.
Wondering what is KHipster - check out here.
You can follow me on Twitter.
If you like this article, please leave a like or a comment. ❤️
Interested to explore further:
-
[Awesome article about Subtyping] (https://eli.thegreenplace.net/2018/covariance-and-contravariance-in-subtyping/)