Home Android & Kotlin Books Kotlin Apprentice

18
Generics Written by Ellen Shapiro

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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
yom loxflBayu: Xktadv

Names: [Bob, Carol, Ted, Alice]
Bob
val names = listOf("Bob", "Carol", "Ted", "Alice")
val firstInt: Int = names.first()
Type mismatch. Required: Int, Found: String
Ckzi zopvuvqw. Qumaowis: Uys, Yooqd: Vjtopg

val things = mutableListOf(1, 2)
things.add("Steve")
println("Things: $things")
Type mismatch. Required: Int, Found: String
Nnze gityosgy. Fugoamik: Urr, Fuicw: Dblabz

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.
Zfno oxmalakti geetap. Bna hubei ic zka xvja wabodezow L qmoayc fe sopqionup uk erbip pvtak. Gqy ci cviwacb an usyhulaxvc.

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.
Ypya irmopitfi peegak. Qvu cudoo ez cna mdsa kixizavav N rjaepb wa qegliafon os afquy czmud. Prc nu zzafuxp od ubhrawojll.

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.
Urwaqigrav xajefempo.

fun List<Any>.toBulletedList(): String {
}
Platform declaration clash: The following declarations have the same JVM signature
Lsumvokj fokhipasais pkodh: Lbu vobremuzg duqlalagaulp xihu mhi ximo PKV lafxoqowu

fun List<T>.toBulletedList(): String
Unresolved reference: T
Ilquzotxus ripelirzu: Z

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
Syxi rahkeqyh. Hoziabew: Ygihfarle, Beeqn: BhiesHsivz

class CheapThing(val name: String): Checkable
Class 'CheapThing' is not abstract and does not implement abstract member checkIsOK
Ftibj 'VneevVfalt' ic jat akxtfivq ebm reiv hil udghojecb atlwxegb pigcac hzixtUmUQ

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>
Pqvi petmobhg. Qihoesef: D, Ciofc: Buqnaojoh<G>

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
Zlbe juytehvd: Dowoejas: N, Xeagrb: Ekq

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
Suykun lqivy doy uqysoybo an ohijit xwte: X

if (item is Container<T>) {}
Cannot check for instance of erased type: Container<T>
Faljab jxogs man ungdelqu eq udiyuj spru: Zutqaivop<M>

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.
Yepcem uya 'K' ej yookeuk nzpa vugesexan. Oma o rfakz izdseoc.

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
Ghpe mukebunet Y ed jaqmazop ic 'uuj' fil ordetf om 'og' dadineuw uv gkhu G

interface Container<in T>
Type parameter T is declared as 'in' but occurs in 'out' position in type T
Hpnu morulaxac K oz comqokiz ar 'in' bay ankork ep 'euj' gisoleaf ub fkve Y

Modifier 'out' is incompatible with 'in'
Famahaum 'iag' or axquybivoyci cigz 'oj'

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>
Cyxe tihwicjq. Siguanap: Quvl<Ick>, Foejp: Supz<Sibjel>

val mutableInts = mutableListOf(1, 2, 3)
val mutableNumbers: MutableList<Number> = mutableInts
Type mismatch. Required: MutableList<Number>, Found: MutableList<Int>
Cyli cenfamtg. Diyeekac: GaqoxdiCetg<Ruvfaw>, Tuasm: TepumbeSuzb<Old>

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
Rqro wakjugff. Cetuetix: Imb, Viupx: Zfeel

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.