Table of contents
1.
Introduction
2.
Generic Type Class Declaration
3.
Generics in Kotlin vs Java 
4.
Need of Generics in Kotlin
5.
Variance
6.
Kotlin in, out and where keywords 
6.1.
The out Keyword:  
6.2.
The in Keyword:
6.3.
The where Keyword:
7.
Covariance 
8.
Type projections
9.
Star Projections
10.
Advantages of Kotlin Generics
11.
FAQs
12.
Key-Takeaways
Last Updated: Mar 27, 2024

Generics in Kotlin

Author Akshit Pant
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Generics are powerful features that allow us to define classes, methods, properties, and many other things that can be accessed by different types. And at compile-time, type discrepancies between classes, methods, and other objects are examined.

In this blog, we will be talking about: Generics in Kotlin, in, out, where keyword, variance, projection, their advantages and their usage.
Before jumping directly on Kotlin Generics, first knowing the basic knowledge of Kotlin and how to use it is strongly recommended.

Generic Type Class Declaration

The parameterized type declaration is used for the generic type class or method. A parameterized type is an instance of a generic type that has actual type arguments attached to it. Angle brackets '< >' are used to declare parameterized types. In most cases, generics are employed in collections.

The syntax for Generics in Kotlin is:

Generic Class:

class<Type>

Generic Method:

<Type>myMethodName(parameters: classType<Type>)

Generics in Kotlin vs Java 

They are a lot similar to those in the Java language, but the Kotlin language authors added unique keywords like ‘out’ and ‘in’ to make them more natural and intelligible. Let's see an example of Kotlin Generics.

Code:

 class myClass<A>(private val ans: A) {
    fun getAns(): A {
        return ans
    }
}

Need of Generics in Kotlin

Let us observe the actual need of the generics with the example given below.

Code:

class myClass(txt: String) {
    var ans = txt
    init{
        println(ans)
    }
}
fun main(args: Array<String>){
    var employee_name: myClass = myClass("Coding Ninjas")
    var employee_salary: myClass = myClass(50000)           // code crashes here           
}

In this example, we are creating a myClass class with the primary constructor having a single parameter. Now, we try to pass the different types of data in the object of myClass class as String and Integer. The primary constructor of myClass class accepts string type ("Coding Ninjas") as the variable employee_name but gives compile-time error when passes Integer type (50000) as the variable employee_salary.

Output:

The integer literal does not conform to the expected type String

To fix the problem, we utilize a generic type class, which is a user-defined class that accepts a variety of parameter types in a single class.

Let's modify the above example with generic types. A class myClass of type T> is a general-purpose type class that accepts both Int and String parameters.

In other words, the type parameter T> serves as a placeholder for the type argument. When the generic type is instantiated, it will be replaced.

Modified Input:

class myClass<T> (txt: T) {
    var ans = txt
    init{
        println(ans)
    }
}
fun main(args: Array<String>){
    var employee_name: myClass<String> = myClass<String>("Coding Ninjas")
    var employee_salary: myClass<Int> = myClass<Int>(50000)                   
}

Output:

Coding Ninjas
50000

Variance

Arrays are invariant by default in Kotlin, unlike Java. Generic types, by extension, are invariant in Kotlin. The out and in keywords can help with this. The property of invariance is that a standard generic function/class that has already been created for a certain data type cannot take or return another data type. All additional data types are supertypes of Any.

We have two types of Variance:

Using in and out(Declaration-site variance), Use-site variance(Type projection)

Kotlin in, out and where keywords 

Let us see how to use inout and where keywords with the examples below.

The out Keyword:  

The 'out' keyword on the generic type in Kotlin allows us to allocate this reference to any of its supertypes. The supplied class can only produce the out value; it cannot be consumed.
The out modifier is known as a variance annotation, and it offers declaration-site variance because it is provided at the type parameter declaration site.

Here is the basic syntax for using out.

Code:

class myOutClass<out T>(val value: T) {
  fun getAns(): T {
  return ans            }                                    }

Here, We've created a class called myOutClass that can generate T values. Then, for the reference that is a supertype of it, we can assign an instance of the OutClass like this.

Syntax:

val out = myOutClass("Int")
val ref: myOutClass<Any> = out

Wonder what would happen if we did not use the out keyword here?

If the out type was not used in the above class, the following set of statements would result in a compiler error.

The in Keyword:

In addition to out, Kotlin has an additional variance annotation called in. It makes a type parameter contravariant, which means it can only be consumed rather than produced.

Here is an example of the in keyword.

Code:

class myInClass<in T> {
    fun findAns(myVal: T): String {
        return myVal.toString()
    }
}

Note that so far, we've declared a findAns() method that will only accept values of type T. Then, we can give the reference of its subtype – Int: a reference of type Number.

val myInClassObj: myInClass<Number> = myInClass()
val ref<Int> = myInClassObj

Similarly here, just like out keyword, If the in type is not utilized in the aforementioned class, the following sentence will result in a compiler error.

The where Keyword:

The default upper bound (if there was none specified) is Any?. Only one upper bound can be specified inside the angle brackets. If the same type parameter needs more than one upper bound, you need a separate where-clause:

Any? is the default upper bound (if none was given). Within the angle brackets, only one upper bound can be provided. A separate where-clause is required if the same type parameter requires more than one upper bound.

Have a look over the syntax with where clause too.

Code:

fun <T> findAns(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
                T : Comparable<T> {
    return list.filter { x > threshold }.map { x.toString() }
}

Here, The passed type must meet all of the where clause's conditions at the same time. The T type must implement both CharSequence and Comparable in the example above.

Covariance 

It indicates that replacing/substituting subtypes is permissible but not supertypes, i.e., a generic function/class created for Number can accept Int, while a generic class defined for Int cannot accept Number.

The out keyword in Kotlin can be used to accomplish this:

 val x: myClass<Any> = myClass<Int>()

This will give a type mismatch error.

 val x: myClass<out Any> = myClass<String>()

This will work since String is a subtype of Any.

 val x: myClass<out String> = myClass<Any>()

This will not work since Any is superType of String.

By appending the out keyword to the declaration site, we may immediately allow covariance. So this will do the work.

Code:

fun main(args: Array<String>) {
    val x: myClass<Any> = myClass<String>()
}
class myClass<out T>

Type projections

It is feasible to copy all the members in an array of some type into an array of any type, but we must annotate the input argument with the out keyword to allow the compiler to compile our code. As a result, the compiler deduces that the input argument can be of any type that is a subtype of the Any type.

Let us see this with an example where we will be copying the elements of one array into another one.

Code:

fun myCopyClass(from: Array<out Any>, to: Array<Any>) {
    for (i in from.indices)    // copy content from one array to another
        to[i] = from[i]
    for (i in to.indices) {    // printing content of new array
    println(to[i])      
    }
}
fun main() {
    val x: Array<Int> = arrayOf(10, 20, 30, 40, 50, 60, 70, 80)
    val y :Array<Any> = Array<Any>(8) { "" }
    myCopyClass(x, y)
}

Output:

10
20
30
40
50
60
70
80

Star Projections

It also happens that sometimes you have no knowledge of the type of argument, but you still want to use it safely. The safest approach is to establish a generic type projection so that every concrete instance of that generic type is a subtype of that projection. So, In simple words, we use the star(*) projection when we don't know the precise type of the item and merely wish to publish all the elements of an array.

Here a simple example of using Star-projections is given below.

Input:

fun myAnswer(array: Array<*>) {
    array.forEach { print(it) }
}
fun main(args :Array<String>) {
    val ans  = arrayOf("Coding ","Ninjas ","Blog ", "Writing")
    myAnswer(ans)
}

Output:

Coding Ninjas Blog Writing

Advantages of Kotlin Generics

Following are the advantages of using Kotlin Generics:

  1. Type-safety: Generic permits only one type of object to be stored. Generic does not allow other objects to be stored.
  2. No need for typecasting: The object does not need to be typecast.
  3. Checking at compile time: Generics code is checked at compile time to ensure that there are no difficulties at runtime.

Must Read Elvis Operator Kotlin

FAQs

  1. In Kotlin, what are Reified types?
    When using the Generics idea to send a class as a parameter to a function and needing to retrieve the type of that class, you must use the reified keyword in Kotlin.
     
  2. What is Contracovariance in Kotlin generics?
    It can be used to put a supertype value in the subtypes, i.e., a generic function/class defined for Number cannot accept supertypes of the datatype it is already defined for. Furthermore, The in keyword is used to implement it in Kotlin.
     
  3. In Kotlin generics, what is the difference between "*" and "Any"?
    List<*> can contain items of any type, but only that kind; for example, it can contain Strings (but only Strings), whereas List<Any> can contain Strings, Integers, and other types of objects in the same list.

Key-Takeaways

In this article, we learned about Kotlin Generics and their types. We also infer from this article about the initialization and implementation of keywords and variance.
However, learning never stops, and there is more to learn. So head over to our Android Development Course on the Coding Ninjas Website to dive deep into Android Development and build future applications.
We hope this article has helped you enhance your knowledge of Kotlin Generics. If you want to learn more, check out our article on environment setup and Competitive Programming articles. Do upvote this article to help other ninjas grow!

Live masterclass