Photo by Vardan Papikyan on Unsplash
What Exactly Are Generics In Kotlin?
Understand the Power of Generics in Kotlin: A Complete Guide.
Introduction
Generics are an important concept in programming, and they are widely used in modern programming languages, including Kotlin. They provide a way to write code that can be used with multiple types of data, making it more flexible and reusable.
In this article, we will be using the analogy of a lunchbox to take a close look at what exactly generics are in Kotlin, how they work, and why they are useful. We will start with a definition of generics, and then explain how they are used in programming languages. We will also give a brief overview of how generics work in Kotlin and provide examples of how they can be used.
By the end of this article, you will have a solid understanding of what generics are, why they are used, and how they can be used in Kotlin. Whether you are a beginner or an experienced programmer, this article will provide valuable insights into this important topic.
Prerequisites
You should be familiar with Object Oriented concepts in Kotlin like classes, inheritance, and interfaces. Knowing how to use a code editor like IntelliJ Idea is also very important.
Definition of Generics
Generics in programming is like a tool that helps you make your code work with many different data types.
Think of a lunchbox as a container for your food, like sandwiches. Now imagine you want to pack a variety of food in the same lunchbox, like fruits or snacks. To make sure you have a balanced and varied lunch, you might use different compartments in your lunchbox for each type of food. That's where generics come in! They allow you to design the lunchbox in such as way that it can hold any type of food, no matter what it is.
Generics in programming work in a similar way. They let you write code that can work with many different types of data, like numbers, words, or even more complex things, without having to change the code for each type of data. This makes your code much more flexible and reusable and helps you write better, more efficient programs.
Why are Generics useful in programming languages?
Improved Code Reuse and Maintainability: Imagine you want to write a program that can store and process different types of data, such as numbers, words, or more complex data structures. Without generics, you would have to write separate code for each type of data, which can be time-consuming and error-prone.
However, with generics, you can write a single piece of code that can work with any type of data you want to use. This makes your code much more flexible and reusable, as you don't have to change the code every time you want to use a different type of data.
Strong Type Inference and Type-Safety: Without strong type inference and type safety, your code might not be clear about what type of data it is working with, which can lead to confusion and errors.
However, with generics and strong type inference, the code knows what type of data it is working with and can enforce the rules for that type of data. This makes your code more reliable and less prone to errors, just like how a well-organized lunchbox helps you avoid mix-ups and mistakes.
Increased Flexibility: If your lunchbox has only one big compartment, where you can only pack only one type of food. This will limit your options and make it difficult to have a balanced and varied lunch.
The same goes for code without generics. Without the ability to handle different types of data, your code becomes limited in what it can do and can be inflexible.
Improved Readability: Code without the use of generics can be difficult to understand and follow, much like a messy lunchbox where all the food is mixed and it's hard to tell what is inside. In contrast, generics make the code easier to read by clearly defining the types of data it is working with. This improved readability increases the code's maintainability and makes it easier to follow and understand.
Understanding Generics in Kotlin
Now that you've understood what generics are in programming languages, I'm sure you are curious to know how that knowledge applies to coding in Kotlin.
"How does generics work in Kotlin?"
"What are some concepts I need to get familiar with?"
I'll be answering these questions shortly.
How does Generics work in Kotlin?
Generics are a special feature in the Kotlin programming language that helps us write better and safer code. They allow us to write code that can handle different types of data, instead of just one type.
What's the syntax for declaring type parameters in Kotlin?
In Kotlin, you can declare generics in a class, interface, function or property by using angle brackets <>
and a type parameter T
.
This type parameter acts like a placeholder, telling the code that it can handle different types of data. When we use the generic class, function, interface, or property, we tell it which type of data we want it to handle.
This way, we can write one generic code that can handle many different types of data, making it more flexible and easier to use. Plus, it helps the computer check for mistakes, so our code runs smoothly and doesn't have any errors.
For example, consider the following class definitions:
class Container<T>(val item: T)
Here, the type parameter T
is declared within the angle brackets <T>
and represents any type of data that the class Container
can work with. When an instance of the class is created, the specific type that the class should work with is specified.
// The generic Container class has been specified to hold integers upon instantiation
val container: Container<Int> = Container<Int>(1)
// The generic Container class has been specified to hold string upon instantiation
val container: Container<String> = Container<String>("Random piece of string")
Consider the class definition below:
class Library<T>(val book: T)
In this example, the type parameter T
represents any type of book that the Library
class can hold. When we create an instance of the class, we specify the type of book that the Library
should hold, like a fiction book, a history book, or a cookbook. The specific type of book is stored in the property book
.
We can create an instance of the class by providing the type argument as shown below
// Given data classes
data class HistoryBook(val title: String, val subtitle: String, val author: String)
data class RomanticBook(val title: String, val subtitle: String, val author: String)
//Instantiating the classes
var historyBook1 = HistoryBook(
title = "What Britain Did To Nigeria",
subtitle = "A Short History of Conquest And Rule",
author = "Max Siollun")
var romanticBook1 = RomanticBook(
title = "Outlander",
subtitle = "",
author = "Diana Gabaldon")
We can use the generic class Library
to store instances of the HistoryBook
and RomanticBook
classes.
val historyLibrary = Library<HistoryBook>(historyBook1)
val romanticLibrary = Library<RomanticBook>(romanticBook1)
Here, historyLibrary
will be a Library
that stores a HistoryBook
instance and romanticLibrary
will be a Library
that stores a RomanticBook
instance. The type parameter T
in the Library
class allows us to specify the type of the book
property at runtime, in this case either HistoryBook
or RomanticBook
.
By using generics, we can write a single class that can hold any type of book, making it more flexible and easier to use. And it helps the computer check for mistakes, so our code runs smoothly without any errors.
Note that type parameter does not have to be named T
. You can use any alphabetic identifier as the type parameter name. For example, you could declare a generic class with a type parameter named E
like this:
class Container<E>(val item: E)
It's common to use uppercase single-letter names such as T
, E
, K
, and V
as type parameter names to indicate that they are types, but you can use any valid identifier as a type parameter name. The important thing is to choose a descriptive name that makes sense in the context of your code.
How do I declare a generic function in Kotlin
To declare a generic function in Kotlin, use the syntax:
fun <TypeParameter> functionName(parameters: TypeParameter): ReturnType {
// function body
}
Consider the function below:
fun <T> printOutValue(value: T) {
println(value)
}
In this example, the printOutValue
function takes in a value of any type T
and prints it to the console. When the function is called, you can specify the type of the value being passed in, for example:
printOutValue(10) // prints 10
printOutValue("Hello") // prints Hello
Constraining Generics in Kotlin
You need to limit the types that can be used with generics in Kotlin because it makes the code better. When you limit the types, your code will only work with the right types, which makes it less likely to have problems.
Kotlin allows you to limit the types that can be used as a generic type. You can do this by setting "bounds" for the type. There are two types of bounds:
Upper bounds: set the highest type that can be used as a generic type.
Lower bounds: set the lowest type that can be used as a generic type.
To set an upper bound, use the <:
symbol. For example, class MyClass<T : Number>
sets an upper bound of Number
for the generic type T
. To set a lower bound, use the >:
symbol.
You can also set bounds on interfaces. For example, interface MyInterface<T> where T : Comparable<T>
sets a bound that the generic type T
must be able to be compared to itself.
Here's an example of a constrained generic function:
fun <T : Comparable<T>> findMax(firstValue: T, secondValue: T): T {
return if (firstValue > secondValue) firstValue else secondValue
}
This function takes two arguments, firstValue
and secondValue
, and returns the maximum of the two. The generic type T
is constrained by the Comparable
interface, meaning that it must be a type that can be compared to itself. This allows the function to use the >
operator to compare the two values and determine the maximum.
The function can be called with arguments of any type that implements the Comparable
interface, for example:
val maximumAge = findMax(25, 30) // returns 30
String
arguments can also be used to call the function since the String
type implements the Comparable
interface.
val maximumName = findMax("Alice", "Bob") // returns "Bob"
Note: The default upper bound (if you don't specify) is Any?
Variance
Variance in generics refers to the relationship between a generic type and its subtypes or supertypes. Variance affects how a generic type can be used in a type argument. There are three types of variance in Kotlin: covariance, contravariance, and invariance.
Covariance
In covariance, a subtype can be used as a supertype but a supertype cannot be used as a subtype. For example, if you have a list of
Novels
, you can use a list ofBooks
instead. Covariant generics allow a generic type to be used in place of a more specific type. To declare a covariant generic type, you use theout
keyword. Here's an exampleinterface Source<out T> { fun nextT(): T }
In this example,
Source<T>
is a covariant generic type, which means that it can be used in place of a more specific type. For example,Source<Book>
can be used in place ofSource<TextBook>
. This means that the following code is valid:val bookSource: Source<Book> = Source<TextBook>()
The reason why covariant generics work this way is that the generic type
T
is only used as a return type and not as a parameter type. This means that the usage ofT
is guaranteed to be safe and cannot result in any type-related errors.Covariant generics can be useful when you have a generic type that produces data, such as a data source, and you want to be able to use it with a more specific type. This allows you to create more flexible and reusable code.
Let's take another example. Consider the following class hierarchy:
open class Vehicle class Car : Vehicle
You can have a
List<Car>
, which is a list of cars, and you can also have aList<Vehicle>
, which is a list of vehicles. Since aCar
is aVehicle
, it makes sense that you can use aList<Vehicle>
wherever aList<Car>
is expected. This is covariance.If you check the documentation on Kotlin
List
, you'll see that the generic type is covariant because it uses theout
keyword, like this:interface List<out E> : Collection<E>
Now because of this covariance, you can use a
List<Vehicle>
wherever aList<Car>
is expected, as in the following example:fun processVehicles(vehicles: List<Vehicle>) {...} val cars: List<Vehicle> = listOf(Car(), Car()) processVehicles(cars) // valid
In this example, you pass a
List<Cars>
to theprocessVehicles
function, which expects aList<Vehicles>
. This works because the generic typeList
is covariant, which means that aList<Car>
can be used as a substitute for aList<Vehicle>
.Contravariance
Contravariance is the opposite of covariance. It can be demonstrated using the
in keyword.
Consider the code below:open class User(id: Int) class NormalUser(): User(1) class AdministrativeUser(): User(2) val adminComparator: Comparable<AdministrativeUser> = object: Comparable<AdministrativeUser> { override fun compareTo(other: AdministrativeUser): Int { return 1 } } val userComperator: Comparable<User> = object: Comparable<User>{ override fun compareTo(other: User): Int { return 1 } } fun main(){ // valid declaration val adminComp: Comparable<AdministrativeUser> = userComperator // Invalid declaration val userComp: Comparable<User> = adminComparator }
The
Comparable
interface is used as an example with two variables,adminComparator
anduserComperator
.adminComparator
is declared as aComparable
ofAdministrativeUser
objects anduserComperator
is declared as aComparable
ofUser
objects.val adminComparator: Comparable<AdministrativeUser> = object : Comparable<AdministrativeUser>{ override fun compareTo(other: AdministrativeUser): Int { return 1 } } val userComperator: Comparable<User> = object: Comparable<User>{ override fun compareTo(other: User): Int { return 1 } }
Since
AdministrativeUser
is a subtype ofUser
, the assignmentval adminComp: Comparable<AdministrativeUser> = userComperator
should be invalid because
Comparable of AdministrativeUser
was required butComparable of User
object was given instead. But the assignment isn't invalid. Why is this so?If we check the Kotlin documentation for the
Comparable interface
, we notice it uses thein keyword
, indicating the use of contravariance. Hence, the same reason why the assignmentval userComp: Comparable<User> = adminComparator
is invalid.
Invariance
By default, generic types are invariant . Invariance refers to the property where a generic type is not considered a subtype or supertype of its parameterized type. In other words, if a class or interface is declared as generic with a type parameter, instances of that class or interface are only considered to be of the same type, and not a subtype or supertype of the type parameter.
Let's consider a scenario where we have a generic class
Person<T>
, whereT
is a type parameter representing the occupation of a person. Since we have declared the class as invariant (because we didn't use thein
orout
keyword, it means that an instance ofPerson<Doctor>
is not considered a subtype or supertype of an instance ofPerson<Teacher>
, even though bothDoctor
andTeacher
are occupations.In other words, an instance of
Person<Doctor>
is only considered to be of typePerson<Doctor>
, and not of typePerson<Teacher>
, or any other type. This means that we cannot assign an instance ofPerson<Doctor>
to a variable of typePerson<Teacher>
, or vice versa.In this scenario, invariance represents the property where a person's occupation is specific and not related to any other occupation. For example, just because a person is a doctor does not mean that they are automatically a teacher, or any other occupation. The same concept applies to the generic class
Person<T>
with invariant type parameterT
.In Kotlin, generics are invariant by default. However, by using the
out
keyword for covariance or thein
keyword for contravariance, we can change the variance of a type parameter and allow for subtyping or supertyping relationships between different instantiations of the generic type.
Final Thoughts
Generics are an important concept in Kotlin, as they allow you to write reusable and type-safe code. Understanding how to use upper bounds, lower bounds, and variance will help you write better, more flexible code that can adapt to different use cases.
Key Points
Generics allow you to write code that works with any type while still providing type safety.
In Kotlin, you can constrain generics using upper bounds, lower bounds, and variance. An upper bound constrain a generic type to only accept a specific subtype or one of its supertypes. A lower bound constrains a generic type to only accept a specific supertype or one of its subtypes.
Variance allows you to indicate whether a generic type is covariant, contravariant, or invariant.
Further Reading
The official Kotlin documentation on generics (https://kotlinlang.org/docs/reference/generics.html)
"Kotlin in Action" by Dmitry Jemerov and Svetlana Isakova (https://www.manning.com/books/kotlin-in-action)
"Programming Kotlin" by Venkat Subramaniam (https://www.oreilly.com/library/view/programming-kotlin/9781492047359/)
References
Official Kotlin documentation. (n.d.). Generics. Retrieved from kotlinlang.org/docs/reference/generics.html
Kotlin For Beginners - Variance, Covariance, Contravariance and Type erasure." (2021, June 22). YouTube. youtube.com/watch?v=yHz-PMooHWg&t=576s