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?

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. Upper bounds: set the highest type that can be used as a generic type.

  2. 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.

  1. 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 of Books 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 the out keyword. Here's an example

     interface 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 of Source<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 of T 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 a List<Vehicle>, which is a list of vehicles. Since a Car is a Vehicle, it makes sense that you can use a List<Vehicle> wherever a List<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 the out keyword, like this:

     interface List<out E> : Collection<E>
    

    Now because of this covariance, you can use a List<Vehicle> wherever a List<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 the processVehicles function, which expects a List<Vehicles>. This works because the generic type List is covariant, which means that a List<Car> can be used as a substitute for a List<Vehicle>.

  2. 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 and userComperator.

    adminComparator is declared as a Comparable of AdministrativeUser objects and userComperator is declared as a Comparable of User 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 of User, the assignment

    val adminComp: Comparable<AdministrativeUser> = userComperator

    should be invalid because Comparable of AdministrativeUser was required but Comparable 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 the in keyword, indicating the use of contravariance. Hence, the same reason why the assignment val userComp: Comparable<User> = adminComparator

    is invalid.

  3. 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>, where T is a type parameter representing the occupation of a person. Since we have declared the class as invariant (because we didn't use the in or out keyword, it means that an instance of Person<Doctor> is not considered a subtype or supertype of an instance of Person<Teacher>, even though both Doctor and Teacher are occupations.

    In other words, an instance of Person<Doctor> is only considered to be of type Person<Doctor>, and not of type Person<Teacher>, or any other type. This means that we cannot assign an instance of Person<Doctor> to a variable of type Person<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 parameter T.

    In Kotlin, generics are invariant by default. However, by using the out keyword for covariance or the in 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

  1. Official Kotlin documentation. (n.d.). Generics. Retrieved from kotlinlang.org/docs/reference/generics.html

  2. Kotlin For Beginners - Variance, Covariance, Contravariance and Type erasure." (2021, June 22). YouTube. youtube.com/watch?v=yHz-PMooHWg&t=576s