Hi folks! Welcome to Kotlin Thursdays. I’m really excited to kick off the first one of the year, and what better way to do it than tackling Generics in Kotlin? Generics in code can actually create stronger type-checking systems in your code, as this gives programmers the ability to fix issues at compile time rather than runtime.

Topics

  • Kotlin Generics: Classes, Types, Generic Classes, Generic Functions, Covariance & Contravariance

Resources

Kotlin Classes and Types

Before we can run, we have to walk first. Let’s start with covering the difference between classes and types in Kotlin. Often, when we hear these two words in Kotlin, they might be used interchangeably; but it turns out, there are differences between the words!

In Kotlin, a type describes properties a set of objects may share, which tells the compiler how the programmer intends to the use the data. A Kotlin class is just an implementation of that type. What this means is that while a class is always a type, a type is not necessarily a class.

Let’s take this non-generic Sloth class for example:

data class Sloth(val name: String, val isTwoFingered: Boolean) {
fun eat() {...}
fun sleep() {...}
}

This sloth doesn’t do very much. I want his life.

So is a Sloth a class or a type? Well, it’s both, as it is a class but also the name of the type. But what about Sloth types saved in variables?

  • var jerryTheSloth: Sloth declares a variable that holds an instance of the Sloth class.
  • var jerryTheSloth: Sloth? shows that Jerry the Sloth can also be declared as a nullable type. Every Kotlin class can be used to construct at least 2 types!

The distinction between types and classes can actually be traced back to programming language theory! Is it a pain? Yes. But it is useful? Also yes, especially when we start talking about Generics.

Generics Type Parameters

As with many generic collections you may see in Kotlin such as Lists, Sets, and Maps, type arguments can be inferred by the Kotlin complier as long as there are objects in the collection.

val slothCrew = listOf(
Sloth("Jerry", false),
Sloth("Bae", true),
Sloth("Chrissy", false)
)

For empty collections, the type will need to be explicitly defined either with

val slothReg: List<Sloth> = listOf()

or

val slothReg = listOf<Sloth>()

Generic Functions

We have our sloth crew, but we all want them to be fed at once. We could write our function to take a list of Sloths:

fun feedCrew(crew: List<Sloth>) {
crew.forEach {
it.eat()
}
}

Jerry, Bae, and Chrissy chillin in my paint masterpiece

This is all fine and dandy, but what if we wanted a panda crew joined the party and we also had to feed them? We couldn’t use this same function for Sloths, so we’d have to write a similar function for Pandas.

data class Sloth(val name: String, val isTwoFingered: Boolean) {    fun eat() {}    fun sleep() {}}

fun feedCrew(crew: List) {
crew.forEach {
it.eat()
}
}

data class Panda(val name: String) {
fun eat() {}
fun sleep() {}
}

fun feedCrew(crew: List) {
crew.forEach {
it.eat()
}
}

If they do the same thing, couldn’t we try to consolidate this function into one? Let’s look into the possibility of using Generic functions to work with any list. We could write a generic feedCrew() function that extends the type Listand when we use it, define the type in the parameter.

The problem is, we can’t actually make the eat() call in this function. This is because as far as the compiler is concerned, not all List types have an eat()function. Here, we can write our function and data classes so that have these two classes inherit from a Mammal class as a subtype.

open class Mammal(val name: String) {    fun eat() {}    fun sleep() {}}

data class Sloths(val slothName: String, val isTwoFingered: Boolean): Mammal(slothName)

data class Pandas(val pandaName: String) : Mammal(pandaName)

fun feedCrew(crew: List) { // List is covariant on its element type
crew.forEach {
it.eat()
}
}

fun main() {
val slothCrew = listOf(
Sloths("Jerry", false),
Sloths("Bae", true),
Sloths("Chrissy", false),
)

val pandaCrew = listOf(
Pandas("Jay"),
Pandas("Peggy")
)

feedCrews(slothCrew)
feedCrews(pandaCrew)
}

Covariance & Contravariance

Why does this really work? If you do Cmd + Click on List in the parameter for feedCrew, we see the signature

public interface List<out E> : Collection<E>

where the list is covariant on its element type.

A covariance class is a generic class where subtyping is preserved. Making a type parameter of a covariant class makes it possible to pass values of that class as function arguments and return values when the type arguments don’t exactly match the ones in the function definition. More specifically, the acceptance criteria for these type arguments when they don’t match is that they are at least the subtype of the defined type argument.

In Kotlin, to declare the class to be covariant on a certain type parameter, you put the out keyword before the name of the type parameter to produce the element type.

Of course, in covariance, this only goes one way. We can say that “a Panda is a subtype of a Mammal” but we can’t say a Mammal is a Sloth.

Subtyping for the class is preserved: Producer<Sloth/Panda> is a subtype of Producer<Mammal>

Contravariance, on the other hand, is a reflection of covariance.

Subtyping for the class is reversed: Consumer<Mammal> is a subtype of Consumer<Sloth/Panda>

In Kotlin, to declare the class to be contravariant on a certain type parameter, you put the in keyword before the name of the type parameter to consumethe element type.

Let’s say we wanted to mash the whole crew together and sort the names in alphabetical order.

Squaaaaadddd

We can use the Comparator interface to be able to do this:

interface Comparator<in T> {
fun compare(e1: T, e2: T): Int {...}

This is what the implementation in the code looks like!

val crewCrewCrew = listOf(        Sloths("Jerry", false),        Sloths("Bae", true),        Sloths("Alex", false),
Pandas("Tegan"),
Pandas("Peggy")
)

val compareByNames = Comparator { a: Mammal, b: Mammal ->
a.name.first().toInt() - b.name.first().toInt()
}

println(crewCrewCrew.sortedWith(compareByNames))

The output, of course, gives us:

[Sloths(slothName=Bae, isTwoFingered=true), Sloths(slothName=Chrissy, isTwoFingered=false), Sloths(slothName=Jerry, isTwoFingered=false), Pandas(pandaName=Peggy), Pandas(pandaName=Tegan)]
I hope you enjoyed this blurb on an introduction in Kotlin Generics! Next week, we will continue on covering Higher-Order Functions and Reified Generics in Kotlin. See you next week!

Views: 604

Happy 10th year, JCertif!

Notes

Welcome to Codetown!

Codetown is a social network. It's got blogs, forums, groups, personal pages and more! You might think of Codetown as a funky camper van with lots of compartments for your stuff and a great multimedia system, too! Best of all, Codetown has room for all of your friends.

When you create a profile for yourself you get a personal page automatically. That's where you can be creative and do your own thing. People who want to get to know you will click on your name or picture and…
Continue

Created by Michael Levin Dec 18, 2008 at 6:56pm. Last updated by Michael Levin May 4, 2018.

Looking for Jobs or Staff?

Check out the Codetown Jobs group.

 

Enjoy the site? Support Codetown with your donation.



InfoQ Reading List

Pinterest Reduces Spark OOM Failures by 96% Through Auto Memory Retries

Pinterest Engineering cut Apache Spark out-of-memory failures by 96% using improved observability, configuration tuning, and automatic memory retries. Staged rollout, dashboards, and proactive memory adjustments stabilized data pipelines, reduced manual intervention, and lowered operational overhead across tens of thousands of daily jobs.

By Leela Kumili

Presentation: Duolingo's Kubernetes Leap

Franka Passing discusses the architectural shift of Duolingo’s 500+ backend services to Kubernetes. She explains the move toward GitOps with Argo CD, the transition to IPv6-only pods, and the "cellular architecture" used to isolate environments. She shares "reports from the trenches" on managing developer trust, navigating AWS rate limits, and productionizing early adopter services.

By Franka Passing

Article: A Better Alternative to Reducing CI Regression Test Suite Sizes

How can you focus in a sea of results from a large regression test suite? This article describes a stochastic approach that relies on some degree of redundancy in your CI regression test set. This approach does not guarantee you will catch every bug every time, but it gives you your best bet of not missing the subtle signatures of all the bugs uncovered by your CI regression test suite runs.

By James Bornefelt Westfall

Podcast: Context Engineering with Adi Polak

In this episode, Thomas Betts and Adi Polak talk about the need for context engineering when interacting with LLMs and designing agentic systems. Prompt engineering techniques work with a stateless approach, while context engineering allows AI systems to be stateful.

By Adi Polak

Dynamic Languages Faster and Cheaper in 13-Language Claude Code Benchmark

A 600-run benchmark by Ruby committer Yusuke Endoh tested Claude Code across 13 languages, implementing a simplified Git. Ruby, Python, and JavaScript were the fastest and cheapest, at $0.36- $0.39 per run. Statistically typed languages cost 1.4-2.6x more. Adding type checkers to dynamic languages imposed 1.6-3.2x slowdowns. Full dataset available on GitHub.

By Steef-Jan Wiggers

© 2026   Created by Michael Levin.   Powered by

Badges  |  Report an Issue  |  Terms of Service