# Creating Generics

Generic code works with types that are “specified later.”

Ordinary classes and functions work with specific types. If you want code to work across more types, this rigidity can be overconstraining.

[Polymorphism](javascript:void(0)) is an object-oriented generalization tool. You write a function that takes a base-class object as a parameter, then call that function with an object of any class derived from that base class—including classes that haven’t yet been created. Now your function is more general, and useful in more places.

A single hierarchy can be too limiting because you must inherit from that hierarchy to produce an object that fits your function parameter. If a function parameter is an interface instead of a class, the limitations are loosened to include anything that implements that interface. This gives the client programmer the option of implementing an interface in combination with an existing class—that is, to adapt an existing class to fit the function. Used this way, interfaces can cut across class hierarchies.

Sometimes even an interface is too restrictive because it forces you to work with only that interface. Your code can be even more general if it works with “some unspecified type,” rather than a particular interface or class. That “unspecified type” is a generic type parameter.

Creating generic types and functions is a fairly complex topic, much of which is outside the scope of this book. This atom attempts to give you just enough background so you aren’t surprised when you come across generic concepts and keywords. If you want to get serious about writing generic types and functions you’ll need to study more advanced resources.

# Any

Any is the root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass. One way to work with unspecified types is by passing Any arguments, and this can sometimes confuse the issue of when to use generics. If Any works, it’s the simpler solution, and simpler is generally better.

There are two ways to use Any. The first, and most straightforward approach, is when you only need to operate on an Any, and nothing more. This is extremely limiting—Any has only three member functions: equals(), hashCode() and toString(). There are also extension functions, but these cannot perform any direct operations on the type. For example, apply() only applies its function argument to the Any.

If you know the type of the Any, you can cast it and perform type-specific operations. Because this involves run-time type information (as shown in [Downcasting](javascript:void(0))), you risk a runtime error if you pass the wrong type to your function (there’s also a slight performance impact). Sometimes this is justified to gain the benefit of eliminating code duplication.

For example, suppose three types each have the ability to communicate. They come from different libraries so you can’t just put them in the same hierarchy, and they have different function names for communicating:

// CreatingGenerics/Speakers.kt
package creatinggenerics
import atomictest.eq

class Person {
  fun speak() = "Hi!"
}

class Dog {
  fun bark() = "Ruff!"
}

class Robot {
  fun communicate() = "Beep!"
}

fun talk(speaker: Any) = when (speaker) {
  is Person -> speaker.speak()
  is Dog -> speaker.bark()
  is Robot -> speaker.communicate()
  else -> "Not a talker" // Or exception
}

fun main() {
  talk(Person()) eq "Hi!"
  talk(Dog()) eq "Ruff!"
  talk(Robot()) eq "Beep!"
  talk(11) eq "Not a talker"
}

The when expression discovers the type of the speaker and calls the appropriate function. If you don’t think talk() will ever need to work with additional types, this is a tolerable solution. Otherwise, it requires you to modify talk() for each new type you add, and to rely on runtime information to discover when you miss something.

# Defining Generics

Duplicated code is a candidate for conversion into a generic function or type. You do this by adding angle brackets (<>) containing one or more generic placeholders. Here, the generic placeholder T represents the unknown type:

// CreatingGenerics/DefiningGenerics.kt
package creatinggenerics

fun <T> gFunction(arg: T): T = arg

class GClass<T>(val x: T) {
  fun f(): T = x
}

class GMemberFunction {
  fun <T> f(arg: T): T = arg
}

interface GInterface<T> {
  val x: T
  fun f(): T
}

class GImplementation<T>(
  override val x: T
) : GInterface<T> {
  override fun f(): T = x
}

class ConcreteImplementation
  : GInterface<String> {
  override val x: String
    get() = "x"
  override fun f() = "f()"
}

fun basicGenerics() {
  gFunction("Yellow")
  gFunction(1)
  gFunction(Dog()).bark()            // [1]
  gFunction<Dog>(Dog()).bark()

  GClass("Cyan").f()
  GClass(11).f()
  GClass(Dog()).f().bark()           // [2]
  GClass<Dog>(Dog()).f().bark()

  GMemberFunction().f("Amber")
  GMemberFunction().f(111)
  GMemberFunction().f(Dog()).bark()  // [3]
  GMemberFunction().f<Dog>(Dog()).bark()

  GImplementation("Cyan").f()
  GImplementation(11).f()
  GImplementation(Dog()).f().bark()

  ConcreteImplementation().f()
  ConcreteImplementation().x
}

basicGenerics() shows that each generic handles different types:

  • gFunction() takes a parameter of type T and returns a T result.
  • GClass stores a T. Its member function f() returns a T.
  • GMemberFunction parameterizes a member function within the class, rather than parameterizing the entire class.
  • You can also define an interface with generic parameters as shown in GInterface. An implementation of GInterface can either redefine a type parameter as in GImplementation, or provide a specific type argument, as in ConcreteImplementation.

Notice in [1], [2] and [3] that we are able to call bark() on the result, because that result emerges as type Dog.

Consider [1], [2] and [3], and the lines immediately following them. The type T is determined by type inference for [1], [2] and [3]. Sometimes this is not possible if a generic or its invocation is too complex to be parsed by the compiler. In this case you must specify the type(s) using the syntax shown in the lines immediately following [1], [2] and [3].

# Preserving Type Information

As you will see later in this atom, code within generic classes and functions can’t know the type of T—this is called erasure. Generics can be thought of as a way to preserve type information for the return value. This way, you don’t have to write code to explicitly check and cast a return value to the desired type.

A common use of generic code is for containers that hold other objects. Consider a CarCrate class that acts as a trivial collection by holding and producing a single element of type Car:

// CreatingGenerics/CarCrate.kt
package creatinggenerics
import atomictest.eq

class Car {
  override fun toString() = "Car"
}

class CarCrate(private var c: Car) {
  fun put(car: Car) { c = car }
  fun get(): Car = c
}

fun main() {
  val cc = CarCrate(Car())
  val car: Car = cc.get()
  car eq "Car"
}

When we call cc.get(), the result comes back as type Car. We’d like to make this tool available to more objects than just Cars, so we generify this class as Crate<T>:

// CreatingGenerics/Crate.kt
package creatinggenerics
import atomictest.eq

open class Crate<T>(private var contents: T) {
  fun put(item: T) { contents = item }
  fun get(): T = contents
}

fun main() {
  val cc = Crate(Car())
  val car: Car = cc.get()
  car eq "Car"
}

Crate<T> ensures that you can only put() a T into the Crate, and when you call get() on that Crate, the result comes back as type T.

We can make a version of map() for Crate by defining a generic extension function:

// CreatingGenerics/MapCrate.kt
package creatinggenerics
import atomictest.eq

fun <T, R> Crate<T>.map(f:(T) -> R): List<R> =
  listOf(f(get()))

fun main() {
  Crate(Car()).map { it.toString() + "x" } eq
    "[Carx]"
}

map() returns the List of results produced by applying f() to each element in the input sequence. Because Crate only contains a single element, the result is always a List of one element. There are two generic arguments: T for the input value and R for the result, allowing f() to produce a result type that is different from the input type.

# Type Parameter Constraints

A type parameter constraint says that the generic argument type must be inherited from the constraint. <T : Base> means that T must be of type Base or something derived from Base. This section shows that using constraints is different from a non-generic type that inherits Base.

Consider a type hierarchy that models different items and ways to dispose of them:

// CreatingGenerics/Disposable.kt
package creatinggenerics
import atomictest.eq

interface Disposable {
  val name: String
  fun action(): String
}

class Compost(override val name: String) :
  Disposable {
  override fun action() = "Add to composter"
}

interface Transport : Disposable

class Donation(override val name: String) :
  Transport {
  override fun action() = "Call for pickup"
}

class Recyclable(override val name: String) :
  Transport {
  override fun action() = "Put in bin"
}

class Landfill(override val name: String) :
  Transport {
  override fun action() = "Put in dumpster"
}

val items = listOf(
  Compost("Orange Peel"),
  Compost("Apple Core"),
  Donation("Couch"),
  Donation("Clothing"),
  Recyclable("Plastic"),
  Recyclable("Metal"),
  Recyclable("Cardboard"),
  Landfill("Trash"),
)

val recyclables =
  items.filterIsInstance<Recyclable>()

Using a constraint, we can access properties and functions of the constrained type within a generic function:

// CreatingGenerics/Constrained.kt
package creatinggenerics
import atomictest.eq

fun <T: Disposable> nameOf(disposable: T) =
  disposable.name

// As an extension:
fun <T: Disposable> T.name() = name

fun main() {
  recyclables.map { nameOf(it) } eq
    "[Plastic, Metal, Cardboard]"
  recyclables.map { it.name() } eq
    "[Plastic, Metal, Cardboard]"
}

We cannot access name without the constraint.

This achieves the same result without generics:

// CreatingGenerics/NonGenericConstraint.kt
package creatinggenerics
import atomictest.eq

fun nameOf2(disposable: Disposable) =
  disposable.name

fun Disposable.name2() = name

fun main() {
  recyclables.map { nameOf2(it) } eq
    "[Plastic, Metal, Cardboard]"
  recyclables.map { it.name2() } eq
    "[Plastic, Metal, Cardboard]"
}

Why use a constraint instead of ordinary polymorphism? The answer is in the return type. With generics, the return type can be exact, rather than being upcast to the base type:

// CreatingGenerics/SameReturnType.kt
package creatinggenerics
import kotlin.random.Random

private val rnd = Random(47)

fun List<Disposable>.aRandom(): Disposable =
  this[rnd.nextInt(size)]

fun <T: Disposable> List<T>.bRandom(): T =
  this[rnd.nextInt(size)]

fun <T> List<T>.cRandom(): T =
  this[rnd.nextInt(size)]

fun sameReturnType() {
  val a: Disposable = recyclables.aRandom()
  val b: Recyclable = recyclables.bRandom()
  val c: Recyclable = recyclables.cRandom()
}

Without generics, aRandom() can only produce a base-class Disposable, while both bRandom() and cRandom() produce a Recyclable. bRandom() never accesses any elements of T, therefore its constraint is pointless and it ends up being the same as cRandom(), which doesn’t use a constraint.

The only time you need constraints is if you require both of the following:

  1. Access a function or property.
  2. Preserve the type when returning it.
// CreatingGenerics/Constraints.kt
package creatinggenerics
import kotlin.random.Random

private val rnd = Random(47)

// Accesses action() but can't
// return the exact type:
fun List<Disposable>.inexact(): Disposable {
  val d: Disposable = this[rnd.nextInt(size)]
  d.action()
  return d
}

// Can't access action() without a constraint:
fun <T> List<T>.noAccess(): T {
  val d: T = this[rnd.nextInt(size)]
  // d.action()
  return d
}

// Access action() and return the exact type:
fun <T: Disposable> List<T>.both(): T {
  val d: T = this[rnd.nextInt(size)]
  d.action()
  return d
}

fun constraints() {
  val i: Disposable = recyclables.inexact()
  val n: Recyclable = recyclables.noAccess()
  val b: Recyclable = recyclables.both()
}

inexact() is an extension to List<Disposable>, which allows it to access action(), but it is not generic so it can only return the base type Disposable. As a generic, noAccess() is able to return the exact type of T, but without a constraint it cannot access action(). Only when you add the constraint on T in both() are you able to access action() and return the exact type T.

# Type Erasure

Java compatibility is an essential part of Kotlin. In Java, generics were not part of the original language—they were added years later, after large bodies of code had been written. Forcing generics into Java without breaking existing code required a crucial compromise: the generic types are only available during compilation but are not preserved at runtime—the types are erased. This erasure affects Kotlin.

Let’s pretend erasure doesn’t happen:

// CreatingGenerics/Erasure.kt
package creatinggenerics

fun main() {
  val strings = listOf("a", "b", "c")
  val all: List<Any> = listOf(1, 2, "x")
  useList(strings)
  useList(all)
}

fun useList(list: List<Any>) {
  // if (list is List<String>) {}  // [1]
}

Uncomment line [1] and you’ll see the following error: “Cannot check for instance of erased type: List<String>”. You can’t test for the generic type at runtime because the type information has been erased.

If erasure didn’t happen, the list might look like this, assuming additional type information is placed at the end of the list (it does not work this way!):

reified generics

Reified Generics

Because generic types are erased, type information is *not* stored in the `List`. Instead, both `strings` and `all` are just `List`s, with no additional type information:
erased generics

Erased Generics

You cannot guess type information from the List contents without analyzing all elements. Checking only the first element from the second list leads you to incorrectly assume that it’s a List<Int>.

The Kotlin designers decided to follow Java and use erasure, for two reasons:

  1. Java compatibility.
  2. Overhead. Storing generic type information significantly increases the memory occupied by a generic List or Map. For example, a standard Map consists of many Map.Entry objects, and Map.Entry is a generic class. Thus, if generics were reified everywhere by default, each key and value of every Map.Entry would contain additional type information.

# Reification of Function Type Arguments

Type information is also erased for generic function calls, which means you can’t do much with a generic parameter inside a function.

To retain type information for function arguments, add the reified keyword. Consider a function a() that requires class information to perform its task:

// CreatingGenerics/ReificationA.kt
package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> a(kClass: KClass<T>) {
  // Uses KClass<T>
}

When we call a() inside a second generic function b(), we would like to use type information for the generic argument:

// CreatingGenerics/ReificationB.kt
package creatinggenerics

// Doesn't compile because of erasure:
// fun <T: Any> b() = a(T::class)

The type information for T is erased when this code runs, so b() won’t compile. You can’t access the class of the generic type parameter inside the function body.

The Java solution is to pass type information into the function by hand:

// CreatingGenerics/ReificationC.kt
package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> c(kClass: KClass<T>) = a(kClass)

class K

val kc = c(K::class)

Passing explicit type information should be redundant because the compiler knows the type of T, and could silently pass it for you. This is effectively what the reified keyword does.

To use reified, the function must also be inline:

// CreatingGenerics/ReificationD.kt
package creatinggenerics

inline fun <reified T: Any> d() = a(T::class)

val kd = d<K>()

d() produces the same effect as c(), but d() doesn’t require the class reference as an argument.

reified tells the compiler to preserve the information about the corresponding type argument. The type information is now available at runtime so you can access it inside the function body.

Reification allows the use of is with a generic parameter type:

// CreatingGenerics/CheckType.kt
package creatinggenerics
import atomictest.eq

inline fun <reified T> check(t: Any) = t is T
// fun <T> check1(t: Any) = t is T     // [1]

fun main() {
  check<String>("1") eq true
  check<Int>("1") eq false
}
  • [1] Without reified, the type information is erased so you can’t check whether a given element is an instance of T.

In the following example, select() produces the name of each Disposable item of a particular subtype. It uses reified combined with a constraint:

// CreatingGenerics/Select.kt
package creatinggenerics
import atomictest.eq

inline fun <reified T : Disposable> select() =
  items.filterIsInstance<T>().map { it.name }

fun main() {
  select<Compost>() eq
    "[Orange Peel, Apple Core]"
  select<Donation>() eq "[Couch, Clothing]"
  select<Recyclable>() eq
    "[Plastic, Metal, Cardboard]"
  select<Landfill>() eq "[Trash]"
}

The library function filterIsInstance() is itself defined using the reified keyword.

# Variance

Combining generics and inheritance produces two dimensions of change. If you have a Container<T> and you want to assign it to a Container<U> where T and U have an inheritance relationship, you must place constraints upon Container using the in or out variance annotations, depending on how you want to use Container.

Here are three versions of a Box container: a basic Box<T>, one using <in T> and one using <out T>:

// CreatingGenerics/InAndOutBoxes.kt
package variance

class Box<T>(private var contents: T) {
  fun put(item: T) { contents = item }
  fun get(): T = contents
}

class InBox<in T>(private var contents: T) {
  fun put(item: T) { contents = item }
}

class OutBox<out T>(private var contents: T) {
  fun get(): T = contents
}

in T means that member functions of the class can only accept arguments of type T, but cannot return values of type T. That is, T objects can be placed into an InBox, but cannot come out.

out T means that member functions can return T objects, but cannot accept arguments of type T—you cannot place T objects into an OutBox.

Why do we need these constraints? Consider this hierarchy:

// CreatingGenerics/Pets.kt
package variance

open class Pet
class Cat : Pet()
class Dog : Pet()

Cat and Dog are both subtypes of Pet. Is there a subtyping relation between Box<Cat> and Box<Pet>? It seems like we should be able to assign, for example, a Box of Cat to a Box of Pet or to a Box of Any (because Any is a supertype of everything):

// CreatingGenerics/BoxAssignment.kt
package variance

val catBox = Box<Cat>(Cat())
// val petBox: Box<Pet> = catBox
// val anyBox: Box<Any> = catBox

If Kotlin allowed this, petBox would have put(item: Pet). Dog is also a Pet, so this would allow you to put a Dog into catBox, violating the “cat-ness” of that Box.

Worse, anyBox would have put(item: Any), so you could put an Any into catBox—the container would have no type safety at all.

If we prevent the use of put(), the assignments are safe because no one can put a Dog into an OutBox<Cat>. The compiler allows us to assign an OutBox<Cat> to an OutBox<Pet> or to an OutBox<Any>, because the out annotation prevents them from having put() functions:

// CreatingGenerics/OutBoxAssignment.kt
package variance

val outCatBox: OutBox<Cat> = OutBox(Cat())
val outPetBox: OutBox<Pet> = outCatBox
val outAnyBox: OutBox<Any> = outCatBox

fun getting() {
  val cat: Cat = outCatBox.get()
  val pet: Pet = outPetBox.get()
  val any: Any = outAnyBox.get()
}

With no put(), we cannot place a Dog into an OutBox<Cat>, so its “cat-ness” is preserved.

Without a get(), an InBox<Any> can be assigned to an InBox<Pet>, an InBox<Cat> or an InBox<Dog>:

// CreatingGenerics/InBoxAssignment.kt
package variance

val inBoxAny: InBox<Any> = InBox(Any())
val inBoxPet: InBox<Pet> = inBoxAny
val inBoxCat: InBox<Cat> = inBoxAny
val inBoxDog: InBox<Dog> = inBoxAny

fun main() {
  inBoxAny.put(Any())
  inBoxAny.put(Pet())
  inBoxAny.put(Cat())
  inBoxAny.put(Dog())

  inBoxPet.put(Pet())
  inBoxPet.put(Cat())
  inBoxPet.put(Dog())

  inBoxCat.put(Cat())
  inBoxDog.put(Dog())
}

It is safe to put() an Any, Pet, Cat or Dog into an InBox<Any>, while you can only put() a Pet, Cat or Dog into an InBox<Pet>. inBoxCat and inBoxDog will only accept Cats and Dogs, respectively. These are the behaviors we expect for boxes that have those type parameters, and the compiler enforces it.

Here’s a summary of the subtyping relationships for Box, OutBox and InBox:

variance

Variance

  • Box<T> is invariant. This means that neither Box<Cat> nor Box<Pet> is a subtype of the other, so neither can be assigned to the other.
  • OutBox<out T> is covariant. This means that OutBox<Cat> is a subtype of OutBox<Pet>. When you upcast an OutBox<Cat> to an OutBox<Pet>, it varies in the same way as upcasting a Cat to a Pet.
  • InBox<in T> is contravariant. This means that InBox<Pet> is a subtype of InBox<Cat>. When you upcast an InBox<Pet> to an InBox<Cat>, it varies in the opposite way as upcasting a Cat to a Pet.

A read-only List from the Kotlin standard library is covariant. You can assign a List<Cat> to a List<Pet>. A MutableList is invariant because it contains an add():

// CreatingGenerics/CovariantList.kt
package variance

fun main() {
  val catList: List<Cat> = listOf(Cat())
  val petList: List<Pet> = catList
  var mutablePetList: MutableList<Pet> =
    mutableListOf(Cat())
  mutablePetList.add(Dog())
  // Type mismatch:
  // mutablePetList =
  //    mutableListOf<Cat>(Cat())  // [1]
}
  • [1] If this assignment worked, we could violate the “cat-ness” of the mutableListOf<Cat> by adding a Dog.

Functions can have covariant return types. This means that an overriding function can return a type that’s more specific than the function it overrides:

// CreatingGenerics/CovariantReturnTypes.kt
package variance

interface Parent
interface Child : Parent

interface  X {
  fun f(): Parent
}

interface Y : X {
  override fun f(): Child
}

Notice how the overridden f() in Y returns a Child, while f() in X returns a Parent.

This subsection has only been a light introduction to the topic of variance.

  • -

Repeated code is a candidate for generic types or functions. This atom only provides a basic grasp of the ideas—if you need deeper understanding you must find it in a more advanced treatment.

Exercises and solutions can be found at www.AtomicKotlin.com.