Chapters

Hide chapters

Kotlin Apprentice

Third Edition · Android 11 · Kotlin 1.4 · IntelliJ IDEA 2020.3

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

18. Generics
Written by Ellen Shapiro

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In programming, centralizing your code is one of the biggest ways to save yourself headaches and prevent bugs. That way, when you’re doing the same thing in multiple places, there’s only one place where those things are actually being done, and only one place where they could possibly break.

A really helpful feature of Kotlin for this is called generics. The general concept of generic programming is that you don’t necessarily need to know exactly what type an object is — or an object associated with the primary object you’re working with — in order to perform actions with or around it. This allows you to combine and simplify functionality in really powerful ways.

Anatomy of standard library generic types

When getting started with generics, it helps to look at the major generic types that are included in Kotlin’s standard library. This way, you can see how the language itself uses this functionality and get some ideas about how you might be able to use it yourself.

Lists

You’ve probably noticed working with List objects that you sometimes need to declare them with the type of item you expect in the list in angle brackets, such as List<String>, or List<Int>.

interface List<out E> : Collection<E>
val names: List<String> = listOf("Bob", "Carol", "Ted", "Alice")
println("Names: $names")
val firstName = names.first()
fun <T> List<T>.first(): T
println(firstName)
val firstName: String
kig goqjdSako: Jnfogr

Names: [Bob, Carol, Ted, Alice]
Bob
val names = listOf("Bob", "Carol", "Ted", "Alice")
val firstInt: Int = names.first()
Type mismatch. Required: Int, Found: String
Dzqo pehkiqrf. Xexeutuf: Exs, Suozm: Hzboxk

val things = mutableListOf(1, 2)
things.add("Steve")
println("Things: $things")
Type mismatch. Required: Int, Found: String
Yrle kucmaxdr. Dexuijat: Etl, Kuolr: Zgnukz

val things: MutableList<Any> = mutableListOf(1, 2)
val things = mutableListOf<Any>(1, 2)
Things: [1, 2, Steve]

Maps

Maps are more complicated than lists because they offer you the opportunity to use not one but two generic types.

interface Map<K, out V>
val map = mapOf(
  Pair("one", 1),
  Pair("two", "II"),
  Pair("three", 3.0f)
)
val one = map.get(1)
Type inference failed. The value of the type parameter K should be mentioned in input types. Try to specify it explicitly.
Bplu algijogya vaeyax. Xpu worie ic sgu sffu qoyekutus R jraedf li xumnaelex is uznem mfqar. Dxf ke zbemuqc uy elkyirucqr.

val one = map[1]
Type inference failed. The value of the type parameter K should be mentioned in input types. Try to specify it explicitly.
Kfpo adtaziqmu saamun. Npu rojei ic qmu btmi bitufenob T gkootd wu dufquoxer up ozquk ckbey. Spf wo wsuyikl iv exyhokurgl.

val valuesForKeysWithE = map.keys
    .filter { it.contains("e") }
    .map { "Value for $it: ${map[it]}" }
println("Values for keys with E: $valuesForKeysWithE")
Values for keys with E: [Value for one: 1, Value for three: 3.0]

Extension functions on types with generic constraints

You’ve been printing out a lot of List objects so far, and you may have noticed they don’t look all that good in the console: They’re always on a single line so it’s difficult to tell what’s actually contained within them or how many objects there are. Say you wanted to print every single line on its own line so that printing a list would look more like this:

- First Item
- Second item
- Third Item
fun List<String>.toBulletedList(): String {
  val separator = "\n - "
  return this.map { "$it" }.joinToString(separator, prefix = separator, postfix = "\n")
}
println("Names: ${names.toBulletedList()}")
println("Values for keys with E: ${valuesForKeysWithE.toBulletedList()}")
Names:
 - Bob
 - Carol
 - Ted
 - Alice

Bob
Things: [1, 2, Steve]
Values for keys with E:
 - Value for one: 1
 - Value for three: 3.0
println("Things: ${things.toBulletedList()}")
Unresolved reference.
Isdirukfeq quhipirto.

fun List<Any>.toBulletedList(): String {
}
Platform declaration clash: The following declarations have the same JVM signature
Shayhakc vinriyujeuj njoww: Vxa soxkihald bumpobujouhx delu vbu dibe SMQ darbexiwu

fun List<T>.toBulletedList(): String
Unresolved reference: T
Ukganixqed hizekisne: C

fun <T> List<T>.toBulletedList(): String
Things:
 - 1
 - 2
 - Steve

Creating your own generic constraints

Another powerful way to use generics is to give generic constraints to classes, functions and variables that you create . This way, you can create something that allows you to operate in a centralized way, but pass in whatever you want for that constraint!

// 1
class Mover<T>(
    // 2
    thingsToMove: List<T>,
    val truckHeightInInches: Int = (12 * 12)
) {

  // 3
  private var thingsLeftInOldPlace = mutableListOf<T>()
  private var thingsInTruck = mutableListOf<T>()
  private var thingsInNewPlace = mutableListOf<T>()

  // 4
  init {
    thingsLeftInOldPlace.addAll(thingsToMove)
  }

  // 5
  fun moveEverythingToTruck() {
    while (thingsLeftInOldPlace.count() > 0) {
      val item = thingsLeftInOldPlace.removeAt(0)
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    }
  }

  // 6
  fun moveEverythingIntoNewPlace() {
    while (thingsInTruck.count() > 0) {
      val item = thingsInTruck.removeAt(0)
	  thingsInNewPlace.add(item)
      println("Moved your $item into your new place!")
    }
  }

  // 7
  fun finishMove() {
    println("OK, we finished! We were able to move your:${thingsInNewPlace.toBulletedList()}")
  }
}
class CheapThing(val name: String) {
  override fun toString(): String {
    return name
  }
}
val cheapThings = listOf(
    CheapThing("Cinder Block table"),
    CheapThing("Box of old books"),
    CheapThing("Ugly old couch")
)
val cheapMover = Mover(cheapThings)
cheapMover.moveEverythingToTruck()  
cheapMover.moveEverythingIntoNewPlace()
cheapMover.finishMove()
Moved your Cinder Block table to the truck!
Moved your Box of old books to the truck!
Moved your Ugly old couch to the truck!
Moved your Cinder Block table into your new place!
Moved your Box of old books into your new place!
Moved your Ugly old couch into your new place!
OK, we finished! We were able to move your:
 - Cinder Block table
 - Box of old books
 - Ugly old couch
class BreakableThing(
    val name: String,
    var isBroken: Boolean = false
) {
  fun smash() {
    isBroken = true
  }

  override fun toString(): String {
    return name
  }
}
val television = BreakableThing("Flat-Screen Television")
val breakableThings = listOf(
      television,
      BreakableThing("Mirror"),
      BreakableThing("Guitar")
  )
val expensiveMover = Mover(breakableThings)
expensiveMover.moveEverythingToTruck()
expensiveMover.moveEverythingIntoNewPlace()
expensiveMover.finishMove()
Moved your Flat-Screen Television to the truck!
Moved your Mirror to the truck!
Moved your Guitar to the truck!
Moved your Flat-Screen Television into your new place!
Moved your Mirror into your new place!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Flat-Screen Television
 - Mirror
 - Guitar
television.smash()
fun moveEverythingToTruck() {
  while (thingsLeftInOldPlace.count() > 0) {
    val item = thingsLeftInOldPlace.removeAt(0)

    if (item is BreakableThing) {
      if (!item.isBroken) {
        thingsInTruck.add(item)
        println("Moved your $item to the truck!")
      } else {
        println("Could not move your $item to the truck")
      }
    } else {
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    }
  }
}

Interfaces

Interfaces allow you to declare information about what something does, rather than what it is, as a class hierarchy would.

interface Checkable {
  fun checkIsOK(): Boolean
}
class Mover<T: Checkable>
private var thingsWhichFailedCheck = mutableListOf<T>()
fun moveEverythingToTruck() {
  while (thingsLeftInOldPlace.count() > 0) {
    val item = thingsLeftInOldPlace.removeAt(0)

    if (item.checkIsOK()) {
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    } else {
      thingsWhichFailedCheck.add(item)
      println("Could not move your $item to the truck :[")
    }
  }
}
fun moveEverythingIntoNewPlace() {
  while (thingsInTruck.count() > 0) {
    val item = thingsInTruck.removeAt(0)
    if (item.checkIsOK()) {
	  thingsInNewPlace.add(item)
	  println("Moved your $item into your new place!")
	} else {
	  thingsWhichFailedCheck.add(item)
	  println("Could not move your $item into your new place :[")
    }
  }
}
fun finishMove() {
  println("OK, we finished! We were able to move your:${thingsInNewPlace.toBulletedList()}")
  if (thingsWhichFailedCheck.isNotEmpty()) {
	println("But we need to talk about your:${thingsWhichFailedCheck.toBulletedList()}")
  }
}
Type mismatch. Required: Checkable, Found: CheapThing
Gljo goqvigpq. Vokaufen: Wmeqcotso, Zoixx: GkuogBsepx

class CheapThing(val name: String): Checkable
Class 'CheapThing' is not abstract and does not implement abstract member checkIsOK
Rviwf 'BboiqRhusx' ad yun enshpebk ejl daor nid ogjruxowc avhqkutw nujsam qzarbOlOH

override fun checkIsOK(): Boolean = true
class BreakableThing(
    val name: String,
    var isBroken: Boolean = false
): Checkable
override fun checkIsOK(): Boolean {
  return !isBroken
}
Moved your Flat-Screen Television to the truck!
Moved your Mirror to the truck!
Moved your Guitar to the truck!
Could not move your Flat-Screen Television into your new place :[
Moved your Mirror into your new place!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Mirror
 - Guitar

But we need to talk about your:
 - Flat-Screen Television

Generic interfaces

A generic interface is an interface that is constrained to a generic type. That can seem like a slightly circular definition when you read it, so what does this look like in practice? Keep going with the moving metaphor.

// 1
interface Container<T> {
  // 2
  fun canAddAnotherItem(): Boolean
  fun addItem(item: T)
  // 3
  fun canRemoveAnotherItem(): Boolean
  fun removeItem(): T
  // 4
  fun getAnother(): Container<T>
  // 5
  fun contents(): List<T>
}
private fun moveContainerToTruck(container: Container<T>) {
  thingsInTruck.add(container)
  println("Moved a container with your ${container.contents().toBulletedList()} to the truck!")
}
Type mismatch. Required: T, Found: Container<T>
Pdse kajduvtn. Xuqaoman: Y, Soimt: Gezziafid<T>

private var thingsInTruck = mutableListOf<Any>()
fun moveEverythingToTruck(startingContainer: Container<T>?)
var currentContainer = startingContainer
currentContainer?.let { moveContainerToTruck(it) }
// 1
if (currentContainer != null) {
  // 2
  if (!currentContainer.canAddAnotherItem()) {
    moveContainerToTruck(currentContainer)
    currentContainer = currentContainer.getAnother()
  }
  // 3
  currentContainer.addItem(item)
  println("Packed your $item!")
} else {
  // 4
  thingsInTruck.add(item)
  println("Moved your $item to the truck!")
}

Type erasure

When a generic type is passed into a class or interface, only information about the generic constraint is actually retained by the compiler, not any information about the concrete type filling in the blank of the generic. This is known as type erasure.

Type mismatch: Required: T, Founds: Any
Zmni geybubdf: Pemuunic: F, Faerzs: Uyp

private fun tryToMoveItemIntoNewPlace(item: T) {
  if (item.checkIsOK()) {
    thingsInNewPlace.add(item)
    println("Moved your $item into your new place!")
  } else {
    thingsWhichFailedCheck.add(item)
    println("Could not move your $item into your new place :[")
  }
}
if (item is T) {}
Cannot check for instance of erased type: T
Gudqed qcakj kul oxnmutdu ek asemek fnki: B

if (item is Container<T>) {}
Cannot check for instance of erased type: Container<T>
Jolzej xnutn val aljcoxnu oc ekodap qxne: Bagbeanup<H>

Star projection

Replace the T in Container<T> with an asterisk:

if (item is Container<*>) {}
if (item is Container<*>) {
  val itemInContainer = item.removeItem()
}

Reified type parameters

Reified generic type parameters allow you to use a generic type, but retain information about that type.

inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>
val breakableThings = thingsInTruck.filterIsInstance<BreakableThing>()

val items = thingsInTruck.filterIsInstance<T>()
Cannot use 'T' as reified type parameter. Use a class instead.
Soqnat ozi 'S' iw taipiog hmva xozehoked. Avu u vhett irbnuoz.

val containers = thingsInTruck.filterIsInstance<Container<*>>()
val containers = thingsInTruck.filterIsInstance<Container<T>>()
for (container in containers) {
  thingsInTruck.remove(container)
  while (container.canRemoveAnotherItem()) {
    val itemInContainer = container.removeItem()
    println("Unpacked your $itemInContainer!")
    tryToMoveItemIntoNewPlace(itemInContainer)
  }
}
while (thingsInTruck.count() > 0) {
  val item = thingsInTruck.removeAt(0) as? T
  if (item != null) {
    tryToMoveItemIntoNewPlace(item)
  } else {
    println("Something in the truck was not of the expected generic type: $item")
  }
}
// 1
class CardboardBox: Container<BreakableThing> {
  //2
  private var items = mutableListOf<BreakableThing>()

  override fun contents(): List<BreakableThing> {
    // 3
    return items.toList()
  }

  // 4
  override fun canAddAnotherItem(): Boolean {
    return items.count() < 2
  }

  override fun addItem(item: BreakableThing) {
    // 5
    items.add(item)
  }

  override fun canRemoveAnotherItem(): Boolean {
    // 6
    return items.count() > 0
  }

  override fun removeItem(): BreakableThing {
    // 7
    val lastItem = items.last()
    items.remove(lastItem)
    return lastItem
  }

  override fun getAnother(): Container<BreakableThing> {
    // 8
    return CardboardBox()
  }
}
cheapMover.moveEverythingToTruck(null)
expensiveMover.moveEverythingToTruck(CardboardBox())
Moved your Cinder Block table to the truck!
Moved your Box of old books to the truck!
Moved your Ugly old couch to the truck!
Moved your Cinder Block table into your new place!
Moved your Box of old books into your new place!
Moved your Ugly old couch into your new place!
OK, we finished! We were able to move your:
 - Cinder Block table
 - Box of old books
 - Ugly old couch
Packed your Flat-Screen Television!
Packed your Mirror!
Moved a container with your
 - Flat-Screen Television
 - Mirror
 to the truck!
Packed your Guitar!
Moved a container with your
 - Guitar
 to the truck!
Unpacked your Mirror!
Moved your Mirror into your new place!
Unpacked your Flat-Screen Television!
Could not move your Flat-Screen Television into your new place :[
Unpacked your Guitar!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Mirror
 - Guitar

But we need to talk about your:
 - Flat-Screen Television

Generic type variance (a.k.a., in and out declarations)

The term generic type variance sounds terrifyingly complex when you first encounter it. This concept is nowhere near as complicated as it sounds.

interface Container<out T>
Type parameter T is declared as 'out' but occurs in 'in' position in type T
Mqwu zimexogus H id duhwipaz el 'aib' mes ebpiqj uh 'oq' xunaduum af cpgi B

interface Container<in T>
Type parameter T is declared as 'in' but occurs in 'out' position in type T
Yzko xewiwiyif N oh zagxunuf ig 'as' jab uldagw uy 'eel' vupadaeh ij pvre D

Modifier 'out' is incompatible with 'in'
Zekitoil 'eiv' al ophetgisahdu wuvt 'it'

interface Container<T>
val ints = listOf(1, 2, 3)
val numbers: List<Number> = ints
val moreInts: List<Int> = numbers
Type mismatch. Required: List<Int>, Found: List<Number>
Bmbo pidkevkk. Pofiofeh: Tihc<Ofj>, Nairz: Kajx<Fipjax>

val mutableInts = mutableListOf(1, 2, 3)
val mutableNumbers: MutableList<Number> = mutableInts
Type mismatch. Required: MutableList<Number>, Found: MutableList<Int>
Ybze vixwustp. Potuuheg: QekalguDisd<Zetqob>, Weepx: BadoglaPufn<Own>

interface Comparable<in T> {
  operator fun compareTo(other: T): Int
}
fun compare(comparator: Comparable<Number>) {
  val int: Int = 1
  comparator.compareTo(int)
  val float: Float = 1.0f
  comparator.compareTo(float)
}
val intComparable: Comparable<Int> = comparator
intComparable.compareTo(int)
intComparable.compareTo(float)
Type mismatch. Required: Int, Found: Float
Mwji berquqlh. Hezooqag: Agl, Qaofx: Vloem

Challenges

Key points

Generics is a gargantuan topic, so review some of the most important things to remember about them in Kotlin:

Where to go from here?

You can go into even more detail on generics than we’ve done here, and I encourage to seek out other resources on topics such as type erasure and variance, for example, to see the differences between the ways variance works in Java and Kotlin.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now