Understanding Generic and Variance in Kotlin

Hello folks! In this blog, we’ll explore some important concepts in Kotlin: Generics and Variance. These concepts are crucial for writing more efficient and concise code. Let’s break it down step by step.

  1. Kotlin code without Generic
  2. Kotlin code with Generic
  3. Generic constraint
  4. Covariant and Contravariant

Kotlin code without Generic

To become a proficient software developer, it’s essential to avoid repetitive code and minimize conditional statements. But how does this relate to Kotlin generics? Stay with me; examples will make it clear.

Imagine you have four remote controls at home: two for the TV and two for the AC. Among them, one TV remote and one AC remote are broken. You want to request the working remote from anyone sitting near one.

Here’s a simple representation of these remotes in Kotlin:

open class Remote {
override fun toString(): String = “Remote”
}

open class TVRemote : Remote() {
override fun toString(): String = “TV Remote”

open fun onRedButtonClicked(){
println(“TV turns ON or OFF”)
}
}

open class ACRemote : Remote() {
override fun toString(): String = “AC Remote”

open fun onONOFFButtonClicked(){
println(“AC turns ON or OFF”)
}
}

class BrokenTVRemote : TVRemote() {
override fun toString(): String = “Broken TV Remote”
override fun onRedButtonClicked() {
println(“TV doesn’t turn ON or OFF”)
}
}

class BrokenACRemote : ACRemote() {
override fun toString(): String = “Broken AC Remote”

override fun onONOFFButtonClicked(){
println(“AC doesn’t turn ON or OFF”)
}
}

In real life, if someone sits near a remote, you ask them for it:

class PersonSatDownNearRemote(private val remote: Remote) {
fun pleaseGiveMeRemote(): Remote = remote
}

Now, your friend Abhilash sits near the TV remote, and Sourabh sits near the AC remote. To use the remotes, you need to check their types and cast them:

fun main() {
val abhilash = PersonSatDownNearRemote(TVRemote())
val remote1 = abhilash.pleaseGiveMeRemote()
if (remote1 is TVRemote){
remote1.onRedButtonClicked()
}

val sourabh = PersonSatDownNearRemote(ACRemote())
val remote2 = sourabh.pleaseGiveMeRemote()
if (remote2 is ACRemote){
remote2.onONOFFButtonClicked()
}
}

This code works, but it involves repetitive type checks and casting. Well-structured code eliminates the need for explicit casting.

Kotlin can help us avoid this using generics.

Kotlin code with Generic

Let’s modify the PersonSatDownNearRemote class to use generics:

class PersonSatDownNearRemote<T>(private val remote: T) {
fun pleaseGiveMeRemote(): T = remote
}

In this code, we introduced a generic type T. We can specify any alphabet or word. as per the naming convention, it should be in uppercase. Now, look at the main function:

fun main() {
val abhilash = PersonSatDownNearRemote(TVRemote())
val remote1 = abhilash.pleaseGiveMeRemote()
remote1.onRedButtonClicked()

val sourabh = PersonSatDownNearRemote(ACRemote())
val remote2 = sourabh.pleaseGiveMeRemote()
remote2.onONOFFButtonClicked()
}

You can see that we no longer need to check the types or cast the remotes explicitly. Kotlin infers the type based on the object passed during creation.

Generic constraint

But what if someone tries to misuse the PersonSatDownNearRemote class by passing something other than a remote, like a String or null?

val sourabh = PersonSatDownNearRemote(“Water bottle”)
val nandan = PersonSatDownNearRemote(null)

We can prevent this by adding a generic constraint:

class PersonSatDownNearRemote<T:Remote>(private val remote: T) {
fun pleaseGiveMeRemote(): T = remote
}

With this constraint, you can only pass Remote and its subclasses as the generic type, preventing misuse.

Before generic constraint Remote, We were able to pass string, null. the reason was

class PersonSatDownNearRemote<T>(private val remote: T) {
fun pleaseGiveMeRemote(): T = remote
}

equivalent to

class PersonSatDownNearRemote<T:Any?>(private val remote: T) {
fun pleaseGiveMeRemote(): T = remote
}

hence always use generic constraint. it provides more detail about our generic type and preventing misuse.

Covariant and Contravariant

Now, let’s discuss covariant and contravariant concepts.

  1. Covariant
  2. Contravariant

Covariant

When we create an instance like this:

val abhilash = PersonSatDownNearRemote(TVRemote())

The variable abhilash has the data type PersonSatDownNearRemote<TVRemote>. However, you can’t assign PersonSatDownNearRemote<TVRemote> to PersonSatDownNearRemote<Remote>. Even though TVRemote is a subclass of Remote, PersonSatDownNearRemote<TVRemote> is not a subclass of PersonSatDownNearRemote<Remote>.

In such cases, you can use covariance, indicated by the out modifier. Covariance allows you to assign a class of a subtype to a class of a supertype. It’s useful for producer classes, which don’t have methods with generic types as input parameters.

Covariant = out = output = Producer class = assign class of subtype to class of supertype

We can use it two way:

declaration-site

class PersonSatDownNearRemote<out T:Remote>(private val remote: T) {
fun pleaseGiveMeRemote(): T = remote
}

or

use-site

Contravariant

Contravariance comes into play when you have a class that consumes generic types. For instance, consider a Dustbin class:

class Dustbin<T:Remote> {
fun throwBrokenRemote(remote: T) {
println(“I will not give you $remote”)
}
}

You can’t assign Dustbin<Remote> to Dustbin<BrokenTVRemote> because you’re trying to assign a supertype to a subtype.

To enable this, you can use contravariance, indicated by the in modifier. Contravariance is suitable for consumer classes, which don’t have methods returning generic types as output.

Contravariant = in = input = Consumer class = assign class of supertype to class of subtype

We can use it two way:

declaration-site

class Dustbin<in T:Remote> {
fun throwBrokenRemote(remote: T) {
println(“I will not give you $remote”)
}
}

or

use-site

This is all above Covariant and Contravariant. always prefer the declaration-site approach as per documentation. These concepts are useful when dealing with classes that produce or consume generic types.

Remember, if a class both produces and consumes generic types. such class is neither Producer class nor Consumer class. we can’t use covariant and contravariant at the declaration site. such a class is Invariant by nature. for such a class we can use covariant and contravariant at the use site(where you’re actually using the class).

for understanding this by example, let’s look at one more class

class CupboardDrawer<T:Remote> {
var remote: T? = null

fun keepRemoteInTheDrawerAfterUse(remote: T) {
println(“Don’t worry your $remote is safe.”)
}

fun giveMeRemoteThatIHaveKept(): T = requireNotNull(
remote, { “you haven’t kept remote. All the best” }
)
}

CupboardDrawer class has a method keepRemoteInTheDrawerAfterUse() that consumes generic type and other method giveMeRemoteThatIHaveKept() that returns generic type as output. so we can say, CupboardDrawer class is invariant.

We are not able to assign drawer3 to drawer1, the reason is, we are trying to assign class of subtype to class of supertype.

and

Here, we are not able to assign drawer3 to drawer2, the reason is we are trying to assign class of supertype to class of subtype.

for solving such a problem we can’t add in or out keywords at the declaration site as CupboardDrawer class is invariant. but we can solve this problem, by adding in and outkeyword at use site on the right side.

if you are still struggling where to write out and in then replace everything in the diamond bracket by Star(*). then our code will look like,

It’s called Start projection.

Congratulations! You’ve now gained a better understanding of how to use out and in keywords correctly with generics in Kotlin.

In Conclusion, Generics can make your code cleaner and more efficient. You just need to understand when and how to use them properly.

For more details on Kotlin generics, check out:

Feel free to share this blog, and you can reach out to me on Twitter, Linkedin, Github for any questions or discussions. Happy coding!

Understanding Generic and Variance in Kotlin was originally published in Walmart Global Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Article Link: Understanding Generic and Variance in Kotlin | by Sagar Kisan Avhad | Walmart Global Tech Blog | Medium